@juspay/neurolink 12.17.1 → 12.17.3

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 (73) hide show
  1. package/CHANGELOG.md +6 -2
  2. package/dist/browser/neurolink.min.js +430 -421
  3. package/dist/cli/commands/proxy.js +455 -259
  4. package/dist/constants/contextWindows.d.ts +2 -0
  5. package/dist/constants/contextWindows.js +4 -0
  6. package/dist/neurolink.js +8 -3
  7. package/dist/providers/openaiChatCompletionsBase.js +60 -51
  8. package/dist/proxy/anthropicBudgetResponse.d.ts +3 -0
  9. package/dist/proxy/anthropicBudgetResponse.js +153 -0
  10. package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
  11. package/dist/proxy/bodyCaptureProcessing.js +23 -4
  12. package/dist/proxy/bodyCaptureWorker.js +20 -2
  13. package/dist/proxy/codexFallback.d.ts +10 -0
  14. package/dist/proxy/codexFallback.js +108 -24
  15. package/dist/proxy/codexUsage.js +2 -0
  16. package/dist/proxy/globalInstaller.d.ts +32 -1
  17. package/dist/proxy/globalInstaller.js +386 -3
  18. package/dist/proxy/otelLogSink.d.ts +3 -3
  19. package/dist/proxy/otelLogSink.js +137 -144
  20. package/dist/proxy/otlpLogTransport.d.ts +3 -0
  21. package/dist/proxy/otlpLogTransport.js +334 -0
  22. package/dist/proxy/proxyActivity.d.ts +22 -2
  23. package/dist/proxy/proxyActivity.js +114 -9
  24. package/dist/proxy/proxyAnalysis.js +82 -52
  25. package/dist/proxy/proxyClientCancellation.d.ts +7 -0
  26. package/dist/proxy/proxyClientCancellation.js +23 -0
  27. package/dist/proxy/proxyContextPreflight.d.ts +25 -0
  28. package/dist/proxy/proxyContextPreflight.js +337 -0
  29. package/dist/proxy/proxyFailureDetails.d.ts +19 -0
  30. package/dist/proxy/proxyFailureDetails.js +96 -0
  31. package/dist/proxy/proxyLifecycle.js +7 -0
  32. package/dist/proxy/proxyRouteBodyCapture.d.ts +13 -0
  33. package/dist/proxy/proxyRouteBodyCapture.js +112 -0
  34. package/dist/proxy/proxyTokenBudget.d.ts +40 -0
  35. package/dist/proxy/proxyTokenBudget.js +523 -0
  36. package/dist/proxy/proxyTracer.d.ts +3 -2
  37. package/dist/proxy/proxyTracer.js +46 -20
  38. package/dist/proxy/proxyTranslationEngine.d.ts +8 -10
  39. package/dist/proxy/proxyTranslationEngine.js +736 -376
  40. package/dist/proxy/requestLogger.d.ts +1 -0
  41. package/dist/proxy/requestLogger.js +107 -5
  42. package/dist/proxy/restartControl.js +9 -4
  43. package/dist/proxy/rollingWorkerProcess.js +65 -0
  44. package/dist/proxy/rollingWorkerSupervisor.js +7 -0
  45. package/dist/proxy/updateCoordinator.d.ts +14 -1
  46. package/dist/proxy/updateCoordinator.js +57 -2
  47. package/dist/server/routes/claudeProxyRoutes.d.ts +4 -5
  48. package/dist/server/routes/claudeProxyRoutes.js +809 -577
  49. package/dist/server/routes/codexProxyRoutes.js +191 -9
  50. package/dist/server/routes/openaiProxyRoutes.d.ts +1 -1
  51. package/dist/server/routes/openaiProxyRoutes.js +261 -69
  52. package/dist/types/cli.d.ts +3 -0
  53. package/dist/types/codex.d.ts +5 -1
  54. package/dist/types/index.d.ts +2 -0
  55. package/dist/types/index.js +2 -0
  56. package/dist/types/proxy.d.ts +148 -3
  57. package/dist/types/proxy.js +0 -15
  58. package/dist/types/proxyBudget.d.ts +74 -0
  59. package/dist/types/proxyBudget.js +1 -0
  60. package/dist/types/proxyContext.d.ts +35 -0
  61. package/dist/types/proxyContext.js +1 -0
  62. package/dist/utils/fileDetector.js +28 -7
  63. package/dist/utils/messageBuilder.js +51 -26
  64. package/dist/utils/pdfProcessor.js +26 -4
  65. package/docs/assets/dashboards/README.md +37 -0
  66. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +197 -89
  67. package/docs-site/static/search-index.json +1 -1
  68. package/package.json +7 -2
  69. package/scripts/observability/NATIVE-DURABLE.md +63 -0
  70. package/scripts/observability/otel-collector.proxy-native-durable.yaml +93 -0
  71. package/scripts/observability/proxy-telemetry-backend.mjs +32 -5
  72. package/scripts/observability/proxy-telemetry-check.mjs +396 -61
  73. package/scripts/observability/query-proxy-history.mjs +34 -5
@@ -37,6 +37,7 @@ export declare function prepareProxyBodyForLogging(body: unknown): {
37
37
  bytes?: number;
38
38
  truncated: boolean;
39
39
  originalBytes?: number;
40
+ unparseableFrames: number;
40
41
  };
41
42
  /** Cheap gate for response observers; avoid copying diagnostics on the serving path. */
42
43
  export declare function isProxyBodyCaptureEnabled(): boolean;
@@ -10,6 +10,8 @@ import { randomUUID } from "node:crypto";
10
10
  import { join } from "path";
11
11
  import { homedir } from "os";
12
12
  import { logger } from "../utils/logger.js";
13
+ import { calculateCost, hasPricing, isExactPricingMatch, } from "../utils/pricing.js";
14
+ import { proxyTokenUsage } from "./proxyTokenUsage.js";
13
15
  import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync, } from "fs";
14
16
  import { writeFile } from "fs/promises";
15
17
  import { setImmediate as yieldToRequests } from "node:timers/promises";
@@ -19,7 +21,7 @@ import { isBorrowedRequest } from "./shareContext.js";
19
21
  import { OtelBridge } from "../observability/otelBridge.js";
20
22
  import { SeverityNumber } from "@opentelemetry/api-logs";
21
23
  import { configureProxyLifecycleLogger } from "./proxyLifecycle.js";
22
- import { notifyProxyFinalLog, notifyProxyAttemptLog } from "./proxyActivity.js";
24
+ import { notifyProxyFinalLog, notifyProxyAttemptLog, getProxyRequestAccounting, } from "./proxyActivity.js";
23
25
  import { withTimeout } from "../utils/async/withTimeout.js";
24
26
  import { resolveProxyLogTraceContext, proxyLogContext, } from "./proxyTraceContext.js";
25
27
  let logDir = null;
@@ -191,7 +193,64 @@ export function initRequestLogger(enabled = true, customLogsDir) {
191
193
  logger.warn(`[proxy] Request logging disabled — failed to create log directory: ${err instanceof Error ? err.message : String(err)}`);
192
194
  }
193
195
  }
196
+ /** Keep price-table estimates distinct from actual subscription billing. */
197
+ function annotateRequestPricing(entry) {
198
+ entry.apiEquivalentCostUsd = null;
199
+ entry.apiEquivalentCacheSavingsUsd = null;
200
+ entry.pricingBasis = "api_price_table";
201
+ entry.pricingProvider = entry.provider ?? "openai-compatible";
202
+ if (entry.usageOwnerRequestId &&
203
+ entry.usageOwnerRequestId !== entry.requestId) {
204
+ entry.pricingStatus = "owned_by_child";
205
+ return;
206
+ }
207
+ const input = entry.inputTokens;
208
+ const output = entry.outputTokens;
209
+ const cacheRead = entry.cacheReadTokens ?? 0;
210
+ const cacheCreation = entry.cacheCreationTokens ?? 0;
211
+ if (input === undefined ||
212
+ output === undefined ||
213
+ !Number.isFinite(input) ||
214
+ input < 0 ||
215
+ !Number.isFinite(output) ||
216
+ output < 0 ||
217
+ !Number.isFinite(cacheRead) ||
218
+ cacheRead < 0 ||
219
+ !Number.isFinite(cacheCreation) ||
220
+ cacheCreation < 0 ||
221
+ (entry.inputIncludesCachedTokens === true &&
222
+ cacheRead + cacheCreation > input)) {
223
+ entry.pricingStatus = "usage_incomplete";
224
+ return;
225
+ }
226
+ if (!hasPricing(entry.pricingProvider, entry.model)) {
227
+ entry.pricingStatus = "unavailable";
228
+ return;
229
+ }
230
+ if (!isExactPricingMatch(entry.pricingProvider, entry.model)) {
231
+ entry.pricingStatus = "inferred";
232
+ return;
233
+ }
234
+ const usage = proxyTokenUsage({
235
+ inputTokens: input,
236
+ outputTokens: output,
237
+ cacheReadTokens: cacheRead,
238
+ cacheCreationTokens: cacheCreation,
239
+ inputIncludesCachedTokens: entry.inputIncludesCachedTokens,
240
+ });
241
+ entry.pricingStatus = "exact";
242
+ entry.apiEquivalentCostUsd = calculateCost(entry.pricingProvider, entry.model, usage);
243
+ const uncachedCost = calculateCost(entry.pricingProvider, entry.model, {
244
+ ...usage,
245
+ input: usage.input + (usage.cacheReadTokens ?? 0),
246
+ cacheReadTokens: 0,
247
+ });
248
+ entry.apiEquivalentCacheSavingsUsd = Math.max(0, Math.round((uncachedCost - entry.apiEquivalentCostUsd) * 1_000_000) /
249
+ 1_000_000);
250
+ }
194
251
  export async function logRequest(entry) {
252
+ Object.assign(entry, getProxyRequestAccounting(entry.requestId));
253
+ annotateRequestPricing(entry);
195
254
  if (!entry.traceId || entry.traceFlags === undefined) {
196
255
  const traceCtx = resolveProxyLogTraceContext(entry);
197
256
  if (traceCtx) {
@@ -206,7 +265,9 @@ export async function logRequest(entry) {
206
265
  : entry.responseStatus >= 400 || entry.errorType
207
266
  ? "handler_error"
208
267
  : "completed";
209
- notifyProxyFinalLog(entry);
268
+ if (!notifyProxyFinalLog(entry)) {
269
+ return;
270
+ }
210
271
  if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
211
272
  return;
212
273
  }
@@ -239,6 +300,7 @@ export async function logRequestAttempt(entry) {
239
300
  entry.traceFlags = traceCtx.traceFlags;
240
301
  }
241
302
  }
303
+ Object.assign(entry, getProxyRequestAccounting(entry.requestId));
242
304
  notifyProxyAttemptLog(entry);
243
305
  if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
244
306
  return;
@@ -306,8 +368,13 @@ function emitOtlpLogRecord(entry) {
306
368
  return;
307
369
  }
308
370
  const otelLogger = provider.getLogger("neurolink-proxy", "1.0.0");
309
- // Determine severity based on response status
310
- const isError = (entry.responseStatus ?? 0) >= 400;
371
+ // A streaming response can fail after sending HTTP 200. Indexed fields
372
+ // and severity must agree with the final semantic outcome in the body.
373
+ const isError = (entry.responseStatus ?? 0) >= 400 ||
374
+ Boolean(entry.errorType) ||
375
+ entry.terminalOutcome === "stream_error" ||
376
+ entry.terminalOutcome === "handler_error" ||
377
+ entry.terminalOutcome === "client_cancelled";
311
378
  const isRateLimit = entry.responseStatus === 429;
312
379
  const severityNumber = isError
313
380
  ? isRateLimit
@@ -324,6 +391,18 @@ function emitOtlpLogRecord(entry) {
324
391
  : `${entry.method} ${entry.path} → ${entry.responseStatus} (${entry.responseTimeMs}ms)`,
325
392
  attributes: {
326
393
  "proxy.record_kind": "request_final",
394
+ "proxy.accounting_scope": entry.accountingScope ?? "client",
395
+ ...(entry.parentRequestId
396
+ ? { "proxy.parent_request_id": entry.parentRequestId }
397
+ : {}),
398
+ ...(entry.usageOwnerRequestId
399
+ ? { "proxy.usage_owner_request_id": entry.usageOwnerRequestId }
400
+ : {}),
401
+ ...(entry.inputIncludesCachedTokens !== undefined
402
+ ? {
403
+ "ai.input_includes_cached_tokens": entry.inputIncludesCachedTokens,
404
+ }
405
+ : {}),
327
406
  // Core request fields
328
407
  "request.id": entry.requestId,
329
408
  "http.method": entry.method,
@@ -334,6 +413,19 @@ function emitOtlpLogRecord(entry) {
334
413
  "ai.model": entry.model,
335
414
  "ai.stream": entry.stream,
336
415
  "ai.tool_count": entry.toolCount,
416
+ "ai.pricing.status": entry.pricingStatus,
417
+ "ai.pricing.basis": entry.pricingBasis,
418
+ "ai.pricing.provider": entry.pricingProvider,
419
+ ...(entry.apiEquivalentCostUsd !== null &&
420
+ entry.apiEquivalentCostUsd !== undefined
421
+ ? { "ai.cost.api_equivalent_usd": entry.apiEquivalentCostUsd }
422
+ : {}),
423
+ ...(entry.apiEquivalentCacheSavingsUsd !== null &&
424
+ entry.apiEquivalentCacheSavingsUsd !== undefined
425
+ ? {
426
+ "ai.cost.cache_savings_usd": entry.apiEquivalentCacheSavingsUsd,
427
+ }
428
+ : {}),
337
429
  // Account info
338
430
  "account.name": entry.account,
339
431
  "account.type": entry.accountType,
@@ -368,7 +460,9 @@ function emitOtlpLogRecord(entry) {
368
460
  ...(entry.traceId && { "trace.id": entry.traceId }),
369
461
  ...(entry.spanId && { "span.id": entry.spanId }),
370
462
  // Derived fields for dashboards (matches backfill script)
371
- is_success: entry.responseStatus === 200,
463
+ is_success: !isError &&
464
+ entry.responseStatus >= 200 &&
465
+ entry.responseStatus < 400,
372
466
  is_rate_limited: entry.responseStatus === 429,
373
467
  is_overloaded: entry.responseStatus === 529,
374
468
  is_error: isError,
@@ -584,11 +678,19 @@ export async function logBodyCapture(entry) {
584
678
  bodyTruncated: stored.bodyTruncated,
585
679
  bodyCaptureLimitBytes: stored.bodyCaptureLimitBytes,
586
680
  originalRedactedBodyBytes: stored.originalRedactedBodyBytes,
681
+ inputRetainedBytes: stored.inputRetainedBytes,
682
+ inputEncoding: stored.inputEncoding,
683
+ sourceTruncated: stored.sourceTruncated,
684
+ processingTruncated: stored.processingTruncated,
685
+ redactionLossy: stored.redactionLossy,
686
+ unparseableRedactedFrames: stored.unparseableRedactedFrames,
587
687
  bodyWriteFailed: stored.bodyWriteFailed,
588
688
  captureError: processed.error,
589
689
  captureAdmission: processed.admission,
590
690
  captureQueueWaitMs: processed.queueWaitMs,
591
691
  captureProcessingMs: processed.processingMs,
692
+ captureAdmissionWaitMs: processed.admissionWaitMs,
693
+ captureWorkerQueueWaitMs: processed.workerQueueWaitMs,
592
694
  metadata: processed.error ? undefined : metadata.metadata,
593
695
  };
594
696
  if (traceCtx) {
@@ -87,7 +87,7 @@ export async function startProxyRestartControl(options) {
87
87
  drainingWorkers: snapshot.draining.length,
88
88
  };
89
89
  };
90
- const assertIdle = () => {
90
+ const assertReplacementAvailable = () => {
91
91
  const snapshot = options.server.snapshot();
92
92
  if (closing || !snapshot.active) {
93
93
  throw new Error("No serving worker is available for a rolling restart.");
@@ -96,8 +96,13 @@ export async function startProxyRestartControl(options) {
96
96
  throw new Error("An update or worker replacement is already in progress.");
97
97
  }
98
98
  if (snapshot.draining.length) {
99
- throw new Error("A previous worker is still finishing requests; another restart is deferred.");
99
+ throw new Error("Previous workers are still draining; another restart is deferred.");
100
100
  }
101
+ // This replaces a worker, not the listener/supervisor. Normal admissions
102
+ // can overlap preflight and activation: the rolling supervisor retains
103
+ // each previous generation until its pending transfers commit, then drains
104
+ // its existing streams. Full quiescence is only required before replacing
105
+ // the supervisor itself (updateCoordinator.isProxyRuntimeSettled).
101
106
  };
102
107
  const operate = async (restart) => {
103
108
  if (busy || closing) {
@@ -118,7 +123,7 @@ export async function startProxyRestartControl(options) {
118
123
  };
119
124
  };
120
125
  try {
121
- assertIdle();
126
+ assertReplacementAvailable();
122
127
  const before = options.server.snapshot();
123
128
  const active = before.active;
124
129
  if (!active) {
@@ -135,7 +140,7 @@ export async function startProxyRestartControl(options) {
135
140
  if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
136
141
  throw new Error("The configured worker executable did not report a valid installed version.");
137
142
  }
138
- assertIdle();
143
+ assertReplacementAvailable();
139
144
  if (options.server.snapshot().active?.pid !== previousWorkerPid) {
140
145
  throw new Error("Serving worker changed during preflight; retry the check.");
141
146
  }
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { handleProxyTokenBudgetMessage, releaseProxyTokenBudgetOwner, } from "./proxyTokenBudget.js";
2
3
  import { ErrorFactory } from "../utils/errorHandling.js";
3
4
  import { isProxyWorkerStatusMessage, PROXY_SOCKET_WORKER_ENV, PROXY_SOCKET_OFFER_TIMEOUT, PROXY_SOCKET_COMMIT_TIMEOUT, } from "./rollingWorkerProtocol.js";
4
5
  /**
@@ -12,6 +13,8 @@ export function spawnProxySocketWorker(options) {
12
13
  const statusListeners = new Set();
13
14
  const pendingStatusMessages = [];
14
15
  let spawnError;
16
+ let budgetResponseFailure;
17
+ let budgetTerminationTimeout;
15
18
  const child = (options.spawn ?? spawn)(options.command, options.args, {
16
19
  env: {
17
20
  ...process.env,
@@ -84,6 +87,38 @@ export function spawnProxySocketWorker(options) {
84
87
  }
85
88
  pending.callback(error);
86
89
  };
90
+ /** Lost budget replies leave unknown leases; release them only after exit. */
91
+ const quarantineBudgetDeliveryFailure = () => {
92
+ if (budgetResponseFailure) {
93
+ return;
94
+ }
95
+ const error = ErrorFactory.proxyWorkerLifecycle(`proxy worker ${childPid} token budget response delivery failed`);
96
+ budgetResponseFailure = error;
97
+ // An IPC-disconnected worker may ignore graceful shutdown. Retain its
98
+ // reservation occupancy until exit, but bound how long it can survive.
99
+ budgetTerminationTimeout = setTimeout(() => {
100
+ if (child.exitCode === null && child.signalCode === null) {
101
+ child.kill("SIGKILL");
102
+ }
103
+ }, 1_000);
104
+ budgetTerminationTimeout.unref?.();
105
+ try {
106
+ publishStatus({
107
+ type: "proxy-worker:fatal",
108
+ generation: options.generation,
109
+ pid: childPid,
110
+ message: error.message,
111
+ });
112
+ for (const socketId of [...pendingSockets.keys()]) {
113
+ settleSocket(socketId, error);
114
+ }
115
+ }
116
+ finally {
117
+ if (child.exitCode === null && child.signalCode === null) {
118
+ child.kill("SIGTERM");
119
+ }
120
+ }
121
+ };
87
122
  /**
88
123
  * Accept messages only from this worker and start a fresh deadline for
89
124
  * the commit phase.
@@ -120,12 +155,35 @@ export function spawnProxySocketWorker(options) {
120
155
  }
121
156
  };
122
157
  child.on("message", (message) => {
158
+ if (budgetResponseFailure) {
159
+ return;
160
+ }
161
+ if (handleProxyTokenBudgetMessage(message, { pid: childPid, generation: options.generation }, (response) => {
162
+ if (!child.connected) {
163
+ quarantineBudgetDeliveryFailure();
164
+ return;
165
+ }
166
+ try {
167
+ child.send(response, (error) => {
168
+ if (error) {
169
+ quarantineBudgetDeliveryFailure();
170
+ }
171
+ });
172
+ }
173
+ catch {
174
+ quarantineBudgetDeliveryFailure();
175
+ }
176
+ })) {
177
+ return;
178
+ }
123
179
  onInternalMessage(message);
124
180
  if (isProxyWorkerStatusMessage(message)) {
125
181
  publishStatus(message);
126
182
  }
127
183
  });
128
184
  child.once("exit", (code, signal) => {
185
+ clearTimeout(budgetTerminationTimeout);
186
+ releaseProxyTokenBudgetOwner(childPid, options.generation);
129
187
  for (const socketId of [...pendingSockets.keys()]) {
130
188
  settleSocket(socketId, ErrorFactory.proxyWorkerLifecycle(`proxy worker ${childPid} exited before socket transfer committed (code=${code ?? "none"}, signal=${signal ?? "none"})`, {
131
189
  workerPid: childPid,
@@ -136,6 +194,9 @@ export function spawnProxySocketWorker(options) {
136
194
  }
137
195
  });
138
196
  const sendControl = (message) => {
197
+ if (budgetResponseFailure) {
198
+ throw budgetResponseFailure;
199
+ }
139
200
  if (!child.connected) {
140
201
  throw new Error(`proxy worker ${childPid} IPC channel is closed`);
141
202
  }
@@ -149,6 +210,10 @@ export function spawnProxySocketWorker(options) {
149
210
  pid: childPid,
150
211
  sendControl,
151
212
  sendSocket: (generation, socket, callback) => {
213
+ if (budgetResponseFailure) {
214
+ callback(budgetResponseFailure);
215
+ return;
216
+ }
152
217
  if (!child.connected) {
153
218
  callback(new Error(`proxy worker ${childPid} IPC channel is closed`));
154
219
  return;
@@ -287,6 +287,13 @@ export class RollingWorkerSupervisor {
287
287
  this.publishState();
288
288
  this.notifyShutdownWaiters();
289
289
  }
290
+ else if (this.draining.has(generation)) {
291
+ // A retiring generation still owns requests and budget leases.
292
+ // Preserve its fatal cause without dropping actual-exit tracking.
293
+ this.recordFailure(generation, expectedVersion, "runtime", message.message, { workerPid: handle.pid });
294
+ this.options.log?.(`[proxy-supervisor] draining worker reported fatal generation=${generation} pid=${handle.pid}: ${message.message}`);
295
+ this.publishState();
296
+ }
290
297
  return;
291
298
  }
292
299
  if (message.type === "proxy-worker:replacement-requested") {
@@ -1,4 +1,4 @@
1
- import type { ProxyUpdateWindowOptions, ProxyUpdateWindowResult } from "../types/index.js";
1
+ import type { ProxyRuntimeActivity, ProxyUpdateWindowOptions, ProxyUpdateWindowResult } from "../types/index.js";
2
2
  /**
3
3
  * A replacement updater starts from the newly installed package. The registry
4
4
  * then reports no newer version even when its launchd parent still runs the
@@ -12,9 +12,22 @@ export declare function shouldRefreshStaleSupervisor(options: {
12
12
  workerVersion?: string;
13
13
  supervisorVersion?: string;
14
14
  }): boolean;
15
+ /** An idle active worker alone does not make its supervisor safe to stop. */
16
+ export declare function isProxyRuntimeSettled(activity: ProxyRuntimeActivity): boolean;
17
+ /** Parse untrusted status conservatively; incomplete rolling evidence is busy. */
18
+ export declare function parseProxyRuntimeActivity(payload: unknown): ProxyRuntimeActivity | null;
15
19
  /**
16
20
  * Prefer a naturally quiet window, then briefly drain new inference traffic.
17
21
  * Existing requests are never interrupted; a bounded drain failure is returned
18
22
  * to the caller so admission can be reopened and retried later.
19
23
  */
20
24
  export declare function waitForProxyUpdateWindow(options: ProxyUpdateWindowOptions): Promise<ProxyUpdateWindowResult>;
25
+ /** An asynchronous staging job must not publish after its service owner changes. */
26
+ export declare function isProxyUpdateOwnerCurrent(options: {
27
+ stopping: boolean;
28
+ parentPid: number;
29
+ updaterPid: number;
30
+ parentStatus: "running" | "not_running" | "unknown";
31
+ runtimePid?: number;
32
+ runtimeUpdaterPid?: number;
33
+ }): boolean;
@@ -11,8 +11,56 @@ export function shouldRefreshStaleSupervisor(options) {
11
11
  typeof options.supervisorVersion === "string" &&
12
12
  options.supervisorVersion !== options.runningVersion);
13
13
  }
14
+ /** An idle active worker alone does not make its supervisor safe to stop. */
15
+ export function isProxyRuntimeSettled(activity) {
16
+ return (activity.activeRequests === 0 &&
17
+ (activity.drainingWorkers ?? 0) === 0 &&
18
+ (activity.queuedSockets ?? 0) === 0 &&
19
+ (activity.pendingTransfers ?? 0) === 0 &&
20
+ (activity.candidateWorkers ?? 0) === 0);
21
+ }
22
+ /** Parse untrusted status conservatively; incomplete rolling evidence is busy. */
23
+ export function parseProxyRuntimeActivity(payload) {
24
+ if (!payload || typeof payload !== "object") {
25
+ return null;
26
+ }
27
+ const value = payload;
28
+ const active = value.activity?.activeRequests;
29
+ if (typeof active !== "number" ||
30
+ !Number.isSafeInteger(active) ||
31
+ active < 0) {
32
+ return null;
33
+ }
34
+ const result = {
35
+ activeRequests: active,
36
+ lastActivityAt: typeof value.activity?.lastActivityAt === "string"
37
+ ? value.activity.lastActivityAt
38
+ : null,
39
+ };
40
+ const rolling = value.autoUpdate?.rolling;
41
+ if (value.autoUpdate?.supervisorPid && !rolling) {
42
+ return null;
43
+ }
44
+ if (rolling) {
45
+ if (!Array.isArray(rolling.draining) ||
46
+ typeof rolling.queuedSockets !== "number" ||
47
+ !Number.isSafeInteger(rolling.queuedSockets) ||
48
+ rolling.queuedSockets < 0 ||
49
+ typeof rolling.pendingTransfers !== "number" ||
50
+ !Number.isSafeInteger(rolling.pendingTransfers) ||
51
+ rolling.pendingTransfers < 0 ||
52
+ rolling.candidate === undefined) {
53
+ return null;
54
+ }
55
+ result.drainingWorkers = rolling.draining.length;
56
+ result.queuedSockets = rolling.queuedSockets;
57
+ result.pendingTransfers = rolling.pendingTransfers;
58
+ result.candidateWorkers = rolling.candidate === null ? 0 : 1;
59
+ }
60
+ return result;
61
+ }
14
62
  function isQuiet(activity, quietThresholdMs, nowMs) {
15
- if (activity.activeRequests > 0) {
63
+ if (!isProxyRuntimeSettled(activity)) {
16
64
  return false;
17
65
  }
18
66
  if (!activity.lastActivityAt) {
@@ -61,7 +109,7 @@ export async function waitForProxyUpdateWindow(options) {
61
109
  }
62
110
  const activity = await options.getActivity();
63
111
  options.onPhase?.("draining", activity);
64
- if (activity?.activeRequests === 0) {
112
+ if (activity && isProxyRuntimeSettled(activity)) {
65
113
  return { ready: true, draining: true };
66
114
  }
67
115
  await wait(options.pollIntervalMs);
@@ -70,3 +118,10 @@ export async function waitForProxyUpdateWindow(options) {
70
118
  options.onPhase?.("drain_timeout", activity);
71
119
  return { ready: false, draining: true, reason: "drain_timeout" };
72
120
  }
121
+ /** An asynchronous staging job must not publish after its service owner changes. */
122
+ export function isProxyUpdateOwnerCurrent(options) {
123
+ return (!options.stopping &&
124
+ options.parentStatus === "running" &&
125
+ options.runtimePid === options.parentPid &&
126
+ options.runtimeUpdaterPid === options.updaterPid);
127
+ }
@@ -2,7 +2,7 @@ import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
2
2
  import { ProxyTracer } from "../../proxy/proxyTracer.js";
3
3
  import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
4
4
  import type { ProxyAccountDirectoryOverride } from "../../types/index.js";
5
- import type { AccountAllowlist, AccountAdmissionLease, JsonObject, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicInvalidRequestFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
5
+ import type { AccountAllowlist, AccountAdmissionLease, JsonObject, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicInvalidRequestFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamBody, AnthropicUpstreamBodyBuilder, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
6
6
  declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined): AccountAdmissionLease | undefined;
7
7
  declare function enqueueAccountAdmission(accountKey: string, capacity: number): QueuedAccountAdmission;
8
8
  declare function acquireAccountAdmission(accountKey: string, capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<AccountAdmissionLease | undefined>;
@@ -269,10 +269,7 @@ declare function handleAnthropicAuthRetry(args: {
269
269
  account: ProxyPassthroughAccount;
270
270
  accountState: RuntimeAccountState;
271
271
  headers: Record<string, string>;
272
- buildUpstreamBody: (token: string) => {
273
- bodyStr: string;
274
- sessionId?: string;
275
- };
272
+ buildUpstreamBody: AnthropicUpstreamBodyBuilder;
276
273
  url: string;
277
274
  enabledAccounts: ProxyPassthroughAccount[];
278
275
  orderedAccounts: ProxyPassthroughAccount[];
@@ -346,9 +343,11 @@ declare function handleAnthropicNonOkResponse(args: {
346
343
  */
347
344
  declare function isAntiAbuseConstruction429(headers: Record<string, string>, body: string): boolean;
348
345
  declare function fetchAnthropicAccountResponse(args: {
346
+ ctx: ServerContext;
349
347
  url: string;
350
348
  headers: Record<string, string>;
351
349
  finalBodyStr: string;
350
+ preparedContext: AnthropicUpstreamBody["preparedContext"];
352
351
  requestedModel?: string;
353
352
  account: ProxyPassthroughAccount;
354
353
  accountState: RuntimeAccountState;