@juspay/neurolink 12.12.4 → 12.12.6

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.
@@ -1,4 +1,5 @@
1
1
  import { ErrorFactory } from "../utils/errorHandling.js";
2
+ import { PROXY_SOCKET_OFFER_TIMEOUT } from "./rollingWorkerProtocol.js";
2
3
  const DEFAULT_READY_TIMEOUT_MS = 30_000;
3
4
  const DEFAULT_SOCKET_QUEUE_LIMIT = 1_024;
4
5
  const DEFAULT_SOCKET_QUEUE_TIMEOUT_MS = 30_000;
@@ -16,6 +17,10 @@ export class RollingWorkerSupervisor {
16
17
  candidate = null;
17
18
  draining = new Map();
18
19
  queuedSockets = [];
20
+ flushingSockets = false;
21
+ consecutiveOfferTimeouts = 0;
22
+ lastStallReplacementAt = 0;
23
+ transferStateTimer;
19
24
  replacement = null;
20
25
  rejectedSockets = 0;
21
26
  failedTransfers = 0;
@@ -29,6 +34,7 @@ export class RollingWorkerSupervisor {
29
34
  ...options,
30
35
  readyTimeoutMs: options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS,
31
36
  socketQueueLimit: options.socketQueueLimit ?? DEFAULT_SOCKET_QUEUE_LIMIT,
37
+ maxPendingTransfers: Math.max(1, options.maxPendingTransfers ?? 16),
32
38
  socketQueueTimeoutMs: options.socketQueueTimeoutMs ?? DEFAULT_SOCKET_QUEUE_TIMEOUT_MS,
33
39
  shutdownTimeoutMs: options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
34
40
  };
@@ -56,6 +62,8 @@ export class RollingWorkerSupervisor {
56
62
  generation: worker.generation,
57
63
  })),
58
64
  queuedSockets: this.queuedSockets.length,
65
+ pendingTransfers: (this.active?.pendingTransfers ?? 0) +
66
+ [...this.draining.values()].reduce((total, worker) => total + worker.pendingTransfers, 0),
59
67
  rejectedSockets: this.rejectedSockets,
60
68
  failedTransfers: this.failedTransfers,
61
69
  recentEvents: [...this.recentEvents],
@@ -93,11 +101,14 @@ export class RollingWorkerSupervisor {
93
101
  this.rejectSocket(socket);
94
102
  return;
95
103
  }
96
- if (this.active) {
104
+ if (this.active &&
105
+ this.queuedSockets.length === 0 &&
106
+ this.active.pendingTransfers < this.options.maxPendingTransfers) {
97
107
  this.transferSocket(this.active, socket);
98
108
  return;
99
109
  }
100
110
  this.queueSocket(socket);
111
+ this.flushQueuedSockets();
101
112
  }
102
113
  queueSocket(socket) {
103
114
  if (this.queuedSockets.length >= this.options.socketQueueLimit) {
@@ -116,7 +127,7 @@ export class RollingWorkerSupervisor {
116
127
  };
117
128
  queued.timeout.unref?.();
118
129
  this.queuedSockets.push(queued);
119
- this.publishState();
130
+ this.scheduleTransferState();
120
131
  }
121
132
  close() {
122
133
  if (this.shutdownPromise) {
@@ -294,6 +305,7 @@ export class RollingWorkerSupervisor {
294
305
  drainRequested: false,
295
306
  };
296
307
  this.active = activated;
308
+ this.consecutiveOfferTimeouts = 0;
297
309
  this.candidate = null;
298
310
  this.flushQueuedSockets();
299
311
  if (previous) {
@@ -365,20 +377,29 @@ export class RollingWorkerSupervisor {
365
377
  });
366
378
  }
367
379
  flushQueuedSockets() {
368
- // Capture the active worker once: a synchronous sendSocket failure inside
369
- // transferSocket can clear this.active mid-loop, and re-reading it would
370
- // pass null into transferSocket and strand the queued socket.
371
- const worker = this.active;
372
- if (!worker) {
380
+ if (this.flushingSockets) {
373
381
  return;
374
382
  }
375
- for (const queued of this.queuedSockets.splice(0)) {
376
- clearTimeout(queued.timeout);
377
- this.transferSocket(worker, queued.socket);
383
+ this.flushingSockets = true;
384
+ try {
385
+ while (this.active &&
386
+ this.queuedSockets.length > 0 &&
387
+ this.active.pendingTransfers < this.options.maxPendingTransfers) {
388
+ const queued = this.queuedSockets.shift();
389
+ if (!queued) {
390
+ break;
391
+ }
392
+ clearTimeout(queued.timeout);
393
+ this.transferSocket(this.active, queued.socket);
394
+ }
395
+ }
396
+ finally {
397
+ this.flushingSockets = false;
378
398
  }
379
399
  }
380
400
  transferSocket(worker, socket) {
381
401
  worker.pendingTransfers += 1;
402
+ this.scheduleTransferState();
382
403
  let settled = false;
383
404
  const complete = (error) => {
384
405
  if (settled) {
@@ -389,7 +410,12 @@ export class RollingWorkerSupervisor {
389
410
  if (error) {
390
411
  this.handleTransferFailure(worker, socket, error);
391
412
  }
413
+ else if (this.active?.generation === worker.generation) {
414
+ this.consecutiveOfferTimeouts = 0;
415
+ }
392
416
  this.maybeDrainWorker(worker);
417
+ this.flushQueuedSockets();
418
+ this.scheduleTransferState();
393
419
  };
394
420
  try {
395
421
  worker.handle.sendSocket(worker.generation, socket, complete);
@@ -426,17 +452,41 @@ export class RollingWorkerSupervisor {
426
452
  reason: detail,
427
453
  });
428
454
  const lifecycle = this.extractLifecycleFailureDetails(error, worker.handle.pid);
455
+ const cancelledOffer = error instanceof Error &&
456
+ error.code === PROXY_SOCKET_OFFER_TIMEOUT;
457
+ if (cancelledOffer && this.active?.generation === worker.generation) {
458
+ this.consecutiveOfferTimeouts += 1;
459
+ // Persistent stalls need recovery, but keep serving existing streams
460
+ // until a replacement activates. Avoid accumulating draining workers or
461
+ // spawning repeatedly when the whole host is under pressure.
462
+ if (this.consecutiveOfferTimeouts >= 3 &&
463
+ !this.candidate &&
464
+ this.draining.size === 0 &&
465
+ Date.now() - this.lastStallReplacementAt >= 60_000 &&
466
+ !this.closed) {
467
+ this.lastStallReplacementAt = Date.now();
468
+ this.options.onReplacementRequested?.({
469
+ generation: worker.generation,
470
+ pid: worker.handle.pid,
471
+ reason: "socket_offer_timeout",
472
+ });
473
+ }
474
+ }
429
475
  this.recordFailure(worker.generation, worker.version, "transfer", `worker ${worker.handle.pid} failed to accept a transferred socket: ${detail}`, {
430
476
  ...lifecycle.details,
431
477
  // If the error already records an exit, the supervisor did not cause
432
478
  // that exit. Otherwise this captures the deliberate cleanup following
433
479
  // the failed transfer, not a claimed root cause for the failure.
434
- supervisorAction: lifecycle.observedExit
435
- ? "none"
436
- : "sigkill_after_transfer_failure",
480
+ supervisorAction: cancelledOffer
481
+ ? "cancel_uncommitted_socket"
482
+ : lifecycle.observedExit
483
+ ? "none"
484
+ : "sigkill_after_transfer_failure",
437
485
  });
438
486
  this.options.log?.(`[proxy-supervisor] socket transfer failed generation=${worker.generation} pid=${worker.handle.pid}: ${detail}`);
439
- if (this.active?.generation === worker.generation && !this.closed) {
487
+ if (!cancelledOffer &&
488
+ this.active?.generation === worker.generation &&
489
+ !this.closed) {
440
490
  this.active = null;
441
491
  this.draining.set(worker.generation, worker);
442
492
  if (!lifecycle.observedExit) {
@@ -518,7 +568,18 @@ export class RollingWorkerSupervisor {
518
568
  this.recentEvents.splice(0, this.recentEvents.length - MAX_RECENT_SUPERVISOR_EVENTS);
519
569
  }
520
570
  }
571
+ scheduleTransferState() {
572
+ if (this.closed || this.transferStateTimer || !this.options.onStateChange) {
573
+ return;
574
+ }
575
+ // The installed supervisor persists each notification. Keep diagnostics
576
+ // fresh without adding a synchronous disk write to every socket handoff.
577
+ this.transferStateTimer = setTimeout(() => this.publishState(), 250);
578
+ this.transferStateTimer.unref();
579
+ }
521
580
  publishState() {
581
+ clearTimeout(this.transferStateTimer);
582
+ this.transferStateTimer = undefined;
522
583
  try {
523
584
  this.options.onStateChange?.(this.snapshot());
524
585
  }
@@ -25,7 +25,8 @@ import { ProviderTransportCoordinator } from "../../proxy/providerTransportCoord
25
25
  import { MAX_COOLDOWN_MS_BY_REASON } from "../../proxy/routingEvidence.js";
26
26
  import { buildProxyLimitHeaders, summarizePoolHeadroom, } from "../../proxy/quotaHeaders.js";
27
27
  import { buildClaudeError, ClaudeStreamSerializer, generateToolUseId, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
28
- import { CodexFallbackResponseError, consumeCodexFallbackResponse, convertClaudeRequestToCodex, } from "../../proxy/codexFallback.js";
28
+ import { CodexFallbackResponseError, consumeCodexFallbackResponse, createCodexFallbackStream, convertClaudeRequestToCodex, } from "../../proxy/codexFallback.js";
29
+ import { registerProxyResponseObserver } from "../../proxy/proxyActivity.js";
29
30
  import { buildAnthropicModelsListResponse, buildTranslationOptions, extractText, extractToolArgs, extractUsageFromStreamResult, handleTranslatedJsonRequest, handleTranslatedStreamRequest, hasTranslatedOutput, } from "../../proxy/proxyTranslationEngine.js";
30
31
  import { tracers } from "../../telemetry/tracers.js";
31
32
  import { withSpan } from "../../telemetry/withSpan.js";
@@ -337,6 +338,7 @@ async function acquireFirstAvailableAccountAdmission(accountKeys, capacity, abor
337
338
  }
338
339
  /** Track whether we've run the one-time startup prune. */
339
340
  let startupPruneDone = false;
341
+ let startupPrune;
340
342
  /** Default cooling period when retries are exhausted and upstream didn't
341
343
  * provide a retry-after header. Short enough to recover quickly, long
342
344
  * enough to avoid immediately hammering the same account. */
@@ -3201,10 +3203,18 @@ async function loadClaudeProxyAccounts(args) {
3201
3203
  const { tokenStore } = await import("../../auth/tokenStore.js");
3202
3204
  const persistedCooldowns = await loadAccountCooldowns();
3203
3205
  if (!startupPruneDone) {
3204
- await tokenStore.pruneExpired();
3205
- startupPruneDone = true;
3206
+ startupPrune ??= tokenStore
3207
+ .pruneExpired()
3208
+ .then(() => {
3209
+ startupPruneDone = true;
3210
+ })
3211
+ .finally(() => {
3212
+ startupPrune = undefined;
3213
+ });
3214
+ await startupPrune;
3206
3215
  }
3207
- const compoundKeys = await tokenStore.listByPrefix("anthropic:");
3216
+ const inventory = await tokenStore.getProviderSnapshot();
3217
+ const compoundKeys = Object.keys(inventory).filter((key) => key.startsWith("anthropic:"));
3208
3218
  // Tracked so an empty pool can name the real cause: "every account is
3209
3219
  // entitlement-blocked" is a different problem from "no credentials".
3210
3220
  const entitlementBlockedLabels = [];
@@ -3215,9 +3225,9 @@ async function loadClaudeProxyAccounts(args) {
3215
3225
  logger.debug(`[proxy] skipping account=${key} (not in account allowlist)`);
3216
3226
  continue;
3217
3227
  }
3218
- if (await tokenStore.isDisabled(key)) {
3228
+ if (inventory[key].disabled) {
3219
3229
  const existingState = getOrCreateRuntimeState(key);
3220
- const disabledReason = await tokenStore.getDisabledReason(key);
3230
+ const disabledReason = inventory[key].disabledReason;
3221
3231
  // Older releases permanently disabled accounts after any refresh error,
3222
3232
  // including timeouts, 429s and 5xx responses. Re-evaluate those legacy
3223
3233
  // entries once under the terminal/transient classifier below.
@@ -3238,7 +3248,7 @@ async function loadClaudeProxyAccounts(args) {
3238
3248
  continue;
3239
3249
  }
3240
3250
  }
3241
- const tokens = await tokenStore.loadTokens(key);
3251
+ const tokens = inventory[key].tokens;
3242
3252
  if (!tokens) {
3243
3253
  skippedForOtherReasons += 1;
3244
3254
  continue;
@@ -3623,9 +3633,8 @@ async function executeClaudeFallbackWithRetry(args) {
3623
3633
  /**
3624
3634
  * Run the configured `codex` fallback through the native pooled Codex route.
3625
3635
  *
3626
- * The inner response is fully buffered and validated before this function
3627
- * creates a single Claude frame. That preserves the proxy's no-replay-after-
3628
- * output guarantee when Codex returns an incomplete stream.
3636
+ * Streaming clients receive incremental output. Once the stream is returned,
3637
+ * failures are terminal SSE errors; only pre-output failures may try a fallback.
3629
3638
  */
3630
3639
  async function executeClaudeCodexFallback(args) {
3631
3640
  const { ctx, body, model, reasoningEffort, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
@@ -3648,6 +3657,145 @@ async function executeClaudeCodexFallback(args) {
3648
3657
  };
3649
3658
  const codexResponse = await handleCodexResponsesRequest(codexCtx);
3650
3659
  const codexHeaders = { ...(codexCtx.responseHeaders ?? {}) };
3660
+ if (body.stream) {
3661
+ const bridge = await createCodexFallbackStream(codexResponse, body.model);
3662
+ ctx.responseHeaders ??= {};
3663
+ Object.assign(ctx.responseHeaders, redactHeadersForBorrower(codexHeaders));
3664
+ const account = codexHeaders["x-neurolink-account"] ?? "";
3665
+ const accountType = codexHeaders["x-neurolink-account-type"] ?? "codex-oauth";
3666
+ let settled = false;
3667
+ let captured = "";
3668
+ let responseBytes = 0;
3669
+ const finish = (status, result, errorType, message) => {
3670
+ if (settled) {
3671
+ return;
3672
+ }
3673
+ settled = true;
3674
+ ctx.abortSignal?.removeEventListener("abort", cancel);
3675
+ if (status >= 400) {
3676
+ ctx.metadata.terminalErrorType = errorType;
3677
+ }
3678
+ tracer?.end(status, Date.now() - requestStartTime);
3679
+ logFinalRequest(status, account, accountType, errorType, message, {
3680
+ inputTokens: result?.usage?.input,
3681
+ outputTokens: result?.usage?.output,
3682
+ cacheCreationTokens: result?.usage?.cacheCreationTokens,
3683
+ cacheReadTokens: result?.usage?.cacheReadTokens,
3684
+ });
3685
+ recordFallbackAttempt({
3686
+ provider: "codex",
3687
+ model,
3688
+ status: status < 400 ? "success" : "failure",
3689
+ durationMs: Date.now() - requestStartTime,
3690
+ ...(message ? { errorMessage: message } : {}),
3691
+ });
3692
+ logProxyBody({
3693
+ phase: "client_response",
3694
+ contentType: "text/event-stream",
3695
+ body: captured,
3696
+ bodySize: responseBytes,
3697
+ responseStatus: status,
3698
+ durationMs: Date.now() - requestStartTime,
3699
+ });
3700
+ };
3701
+ const cancel = () => {
3702
+ finish(499, undefined, "client_cancelled", "Client cancelled Codex fallback stream");
3703
+ void bridge.cancel();
3704
+ };
3705
+ ctx.abortSignal?.addEventListener("abort", cancel, { once: true });
3706
+ registerProxyResponseObserver(ctx.metadata, {
3707
+ onTerminal: ({ outcome }) => {
3708
+ if (outcome === "client_cancelled") {
3709
+ cancel();
3710
+ }
3711
+ else if (outcome === "stream_error") {
3712
+ finish(502, undefined, "stream_error", "Codex fallback stream failed");
3713
+ void bridge.cancel();
3714
+ }
3715
+ },
3716
+ });
3717
+ const capture = (frame) => {
3718
+ responseBytes += Buffer.byteLength(frame);
3719
+ if (captured.length < 1024 * 1024) {
3720
+ captured += frame.slice(0, 1024 * 1024 - captured.length);
3721
+ }
3722
+ return frame;
3723
+ };
3724
+ async function* relay() {
3725
+ try {
3726
+ if (ctx.abortSignal?.aborted) {
3727
+ cancel();
3728
+ return;
3729
+ }
3730
+ let pending = bridge.frames.next();
3731
+ while (!settled) {
3732
+ let timer;
3733
+ const heartbeat = new Promise((resolve) => {
3734
+ timer = setTimeout(() => resolve(null), 15_000);
3735
+ timer.unref();
3736
+ });
3737
+ let next;
3738
+ try {
3739
+ next = await Promise.race([pending, heartbeat]);
3740
+ }
3741
+ finally {
3742
+ clearTimeout(timer);
3743
+ }
3744
+ if (settled) {
3745
+ return;
3746
+ }
3747
+ if (next === null) {
3748
+ yield capture(ClaudeStreamSerializer.pingEvent());
3749
+ continue;
3750
+ }
3751
+ if (next.done === true) {
3752
+ finish(200, next.value);
3753
+ return;
3754
+ }
3755
+ const frame = capture(next.value);
3756
+ if (frame.startsWith("event: message_stop\n")) {
3757
+ // Finalize before exposing the terminal frame: a client can close
3758
+ // immediately after receiving it without making another pull.
3759
+ const completion = await bridge.frames.next();
3760
+ if (completion.done !== true) {
3761
+ throw new Error("Codex fallback emitted output after message_stop");
3762
+ }
3763
+ if (settled) {
3764
+ return;
3765
+ }
3766
+ finish(200, completion.value);
3767
+ yield frame;
3768
+ return;
3769
+ }
3770
+ yield frame;
3771
+ pending = bridge.frames.next();
3772
+ }
3773
+ }
3774
+ catch (error) {
3775
+ if (!settled) {
3776
+ const detail = redactProviderErrorMessage(describeTransportError(error));
3777
+ logger.always(`[proxy] Codex fallback stream failed: ${detail}`);
3778
+ const serializer = new ClaudeStreamSerializer(body.model);
3779
+ const frames = [
3780
+ ...serializer.emitError(502, "Codex fallback stream failed"),
3781
+ ].map(capture);
3782
+ finish(502, undefined, "stream_error", detail);
3783
+ yield* frames;
3784
+ }
3785
+ }
3786
+ finally {
3787
+ ctx.abortSignal?.removeEventListener("abort", cancel);
3788
+ if (!settled) {
3789
+ cancel();
3790
+ }
3791
+ await bridge.cancel();
3792
+ await bridge.frames
3793
+ .return({ text: "", toolCalls: [], finishReason: "end_turn" })
3794
+ .catch(() => undefined);
3795
+ }
3796
+ }
3797
+ return relay();
3798
+ }
3651
3799
  let parsed;
3652
3800
  try {
3653
3801
  parsed = await consumeCodexFallbackResponse(codexResponse);
@@ -3673,49 +3821,6 @@ async function executeClaudeCodexFallback(args) {
3673
3821
  ...(parsed.usage ? { usage: parsed.usage } : {}),
3674
3822
  toolCalls: parsed.toolCalls,
3675
3823
  };
3676
- if (body.stream) {
3677
- const serializer = new ClaudeStreamSerializer(body.model, parsed.usage?.input ?? 0);
3678
- const frames = [];
3679
- for (const frame of serializer.start()) {
3680
- frames.push(frame);
3681
- }
3682
- if (parsed.text) {
3683
- for (const frame of serializer.pushDelta(parsed.text)) {
3684
- frames.push(frame);
3685
- }
3686
- }
3687
- for (const toolCall of parsed.toolCalls) {
3688
- for (const frame of serializer.pushToolUse(generateToolUseId(), toolCall.toolName, toolCall.args)) {
3689
- frames.push(frame);
3690
- }
3691
- }
3692
- for (const frame of serializer.finish(parsed.usage?.output, parsed.finishReason)) {
3693
- frames.push(frame);
3694
- }
3695
- tracer?.end(200, Date.now() - requestStartTime);
3696
- logFinalRequest(200, accountLabel, accountType, undefined, undefined, {
3697
- inputTokens: parsed.usage?.input,
3698
- outputTokens: parsed.usage?.output,
3699
- cacheCreationTokens: parsed.usage?.cacheCreationTokens,
3700
- cacheReadTokens: parsed.usage?.cacheReadTokens,
3701
- });
3702
- const bufferedBody = frames.join("");
3703
- logProxyBody({
3704
- phase: "client_response",
3705
- headers: { "content-type": "text/event-stream" },
3706
- body: bufferedBody,
3707
- bodySize: Buffer.byteLength(bufferedBody, "utf8"),
3708
- contentType: "text/event-stream",
3709
- responseStatus: 200,
3710
- durationMs: Date.now() - requestStartTime,
3711
- });
3712
- async function* sseGenerator() {
3713
- for (const frame of frames) {
3714
- yield frame;
3715
- }
3716
- }
3717
- return sseGenerator();
3718
- }
3719
3824
  tracer?.end(200, Date.now() - requestStartTime);
3720
3825
  const clientResponse = serializeClaudeResponse(internal, body.model);
3721
3826
  logFinalRequest(200, accountLabel, accountType, undefined, undefined, {
@@ -3820,6 +3925,7 @@ async function tryConfiguredClaudeFallbackChain(args) {
3820
3925
  reason: "all_anthropic_accounts_exhausted",
3821
3926
  });
3822
3927
  let lastFallbackError;
3928
+ let terminalFailure;
3823
3929
  let invalidRequestFailure;
3824
3930
  for (const fallback of fallbackPlan.attempts.slice(1)) {
3825
3931
  if (!fallback.provider || !fallback.model) {
@@ -3873,6 +3979,9 @@ async function tryConfiguredClaudeFallbackChain(args) {
3873
3979
  providerLabel: fallback.provider,
3874
3980
  });
3875
3981
  }
3982
+ if (fallback.provider === "codex" && body.stream) {
3983
+ return { response };
3984
+ }
3876
3985
  recordFallbackAttempt({
3877
3986
  provider: fallback.provider,
3878
3987
  model: fallback.model,
@@ -3899,7 +4008,27 @@ async function tryConfiguredClaudeFallbackChain(args) {
3899
4008
  return { response };
3900
4009
  }
3901
4010
  catch (fallbackErr) {
3902
- invalidRequestFailure ??=
4011
+ const status = ctx.abortSignal?.aborted
4012
+ ? 499
4013
+ : fallbackErr instanceof CodexFallbackResponseError
4014
+ ? fallbackErr.status
4015
+ : 502;
4016
+ terminalFailure = {
4017
+ status,
4018
+ message: `Configured fallback ${fallback.provider}/${fallback.model} failed (HTTP ${status})`,
4019
+ errorType: status === 499
4020
+ ? "client_cancelled"
4021
+ : status === 429
4022
+ ? "rate_limit_error"
4023
+ : status === 401
4024
+ ? "authentication_error"
4025
+ : status === 403
4026
+ ? "permission_error"
4027
+ : status === 400
4028
+ ? "invalid_request_error"
4029
+ : "api_error",
4030
+ };
4031
+ invalidRequestFailure =
3903
4032
  getCodexFallbackInvalidRequestFailure(fallbackErr) ?? undefined;
3904
4033
  const errMsg = redactProviderErrorMessage(fallbackErr instanceof Error
3905
4034
  ? fallbackErr.message
@@ -3934,14 +4063,29 @@ async function tryConfiguredClaudeFallbackChain(args) {
3934
4063
  durationMs: Date.now() - fallbackStart,
3935
4064
  });
3936
4065
  lastFallbackError = `[${fallback.provider}/${fallback.model}] ${redactProviderErrorMessage(describeTransportError(fallbackErr))}`;
4066
+ if (ctx.abortSignal?.aborted) {
4067
+ break;
4068
+ }
3937
4069
  }
3938
4070
  }
3939
4071
  return {
3940
4072
  response: null,
3941
4073
  lastErrorMessage: lastFallbackError,
4074
+ terminalFailure,
3942
4075
  ...(invalidRequestFailure ? { invalidRequestFailure } : {}),
3943
4076
  };
3944
4077
  }
4078
+ /** Preserve the final fallback status through every HTTP route adapter. */
4079
+ function buildConfiguredClaudeFallbackFailure(args) {
4080
+ const { failure, buildLoggedClaudeError, tracer, requestStartTime } = args;
4081
+ tracer?.setError(failure.errorType, failure.message);
4082
+ tracer?.end(failure.status, Date.now() - requestStartTime);
4083
+ const body = buildLoggedClaudeError(failure.status, failure.message, failure.errorType);
4084
+ return new Response(JSON.stringify(body), {
4085
+ status: failure.status,
4086
+ headers: { "content-type": "application/json" },
4087
+ });
4088
+ }
3945
4089
  async function tryAutoClaudeFallback(args) {
3946
4090
  const { ctx, body, tracer, requestStartTime, logProxyBody, logFinalRequest } = args;
3947
4091
  const fallbackStart = Date.now();
@@ -6325,6 +6469,15 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6325
6469
  logFinalRequest,
6326
6470
  });
6327
6471
  }
6472
+ if (configuredFallbackResult.terminalFailure) {
6473
+ const failure = configuredFallbackResult.terminalFailure;
6474
+ return buildConfiguredClaudeFallbackFailure({
6475
+ failure,
6476
+ buildLoggedClaudeError,
6477
+ tracer,
6478
+ requestStartTime,
6479
+ });
6480
+ }
6328
6481
  return buildDeferredClaudeAccountFailureResponse({
6329
6482
  ctx,
6330
6483
  tracer,
@@ -6821,13 +6974,25 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6821
6974
  if (configuredFallbackResult.response) {
6822
6975
  return configuredFallbackResult.response;
6823
6976
  }
6824
- if (configuredFallbackResult.invalidRequestFailure &&
6825
- !loopState.sawRateLimit) {
6826
- // A converted Codex request can be rejected independently of the original
6827
- // Anthropic request. Preserve a real pool 429 as the actionable terminal
6828
- // response when both occurred.
6829
- loopState.invalidRequestFailure =
6830
- configuredFallbackResult.invalidRequestFailure;
6977
+ if (configuredFallbackResult.invalidRequestFailure) {
6978
+ // Surface the failure of the provider actually attempted last.
6979
+ return buildClaudeAnthropicFailureResponse({
6980
+ tracer,
6981
+ requestStartTime,
6982
+ authFailureMessage: null,
6983
+ authCooldownMessage: null,
6984
+ invalidRequestFailure: configuredFallbackResult.invalidRequestFailure,
6985
+ entitlementFailure: null,
6986
+ scopedExhaustion: null,
6987
+ sawNetworkError: false,
6988
+ sawTransientFailure: false,
6989
+ sawRateLimit: false,
6990
+ lastError: undefined,
6991
+ orderedAccounts: [],
6992
+ buildLoggedClaudeError,
6993
+ logProxyBody,
6994
+ logFinalRequest,
6995
+ });
6831
6996
  }
6832
6997
  fallbackFailureMessage = configuredFallbackResult.lastErrorMessage;
6833
6998
  // A translation-layer-selected provider is only permitted by an explicit
@@ -6849,6 +7014,16 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6849
7014
  fallbackFailureMessage =
6850
7015
  autoFallbackResult.lastErrorMessage ?? fallbackFailureMessage;
6851
7016
  }
7017
+ if (configuredFallbackResult.terminalFailure &&
7018
+ !configuredFallbackResult.invalidRequestFailure) {
7019
+ const failure = configuredFallbackResult.terminalFailure;
7020
+ return buildConfiguredClaudeFallbackFailure({
7021
+ failure,
7022
+ buildLoggedClaudeError,
7023
+ tracer,
7024
+ requestStartTime,
7025
+ });
7026
+ }
6852
7027
  loopState.fallbackFailureMessage = fallbackFailureMessage;
6853
7028
  }
6854
7029
  // Terminal failure — usually "every account is rate-limited". This is the
@@ -134,18 +134,18 @@ function buildCodexErrorResponse(status, message) {
134
134
  * tokens and hydrating cooldown + quota state from disk.
135
135
  */
136
136
  async function loadCodexProxyAccounts() {
137
- const keys = await tokenStore.listByPrefix(CODEX_ACCOUNT_PREFIX);
138
- const [cooldowns, quotas] = await Promise.all([
137
+ const [inventory, cooldowns, quotas] = await Promise.all([
138
+ tokenStore.getProviderSnapshot(),
139
139
  loadAccountCooldowns(),
140
140
  loadAccountQuotas(),
141
141
  ]);
142
142
  const now = Date.now();
143
143
  const accounts = [];
144
- for (const key of keys) {
145
- if (await tokenStore.isDisabled(key)) {
144
+ for (const [key, entry] of Object.entries(inventory)) {
145
+ if (!key.startsWith(CODEX_ACCOUNT_PREFIX) || entry.disabled) {
146
146
  continue;
147
147
  }
148
- const tokens = await tokenStore.loadTokens(key);
148
+ const tokens = entry.tokens;
149
149
  if (!tokens || tokens.tokenType !== "Bearer") {
150
150
  // Only OAuth (Bearer) accounts can serve the ChatGPT backend.
151
151
  continue;
@@ -346,6 +346,17 @@ export async function handleCodexResponsesRequest(ctx) {
346
346
  }).catch(() => undefined);
347
347
  };
348
348
  const accounts = await loadCodexProxyAccounts();
349
+ const cancelRequest = async (account) => {
350
+ await recordFinalOutcome(account, 499, {
351
+ errorType: "client_cancelled",
352
+ errorMessage: "Client cancelled Codex request",
353
+ terminalOutcome: "client_cancelled",
354
+ });
355
+ return buildCodexErrorResponse(499, "Client cancelled Codex request");
356
+ };
357
+ if (ctx.abortSignal?.aborted) {
358
+ return cancelRequest();
359
+ }
349
360
  if (accounts.length === 0) {
350
361
  await recordFinalOutcome(undefined, 401, {
351
362
  errorType: "no_accounts",
@@ -392,6 +403,9 @@ export async function handleCodexResponsesRequest(ctx) {
392
403
  let authRetried = false;
393
404
  // Same-account loop only re-runs once, for a post-401 token refresh.
394
405
  for (;;) {
406
+ if (ctx.abortSignal?.aborted) {
407
+ return cancelRequest(lastAttemptedAccount);
408
+ }
395
409
  attempt += 1;
396
410
  const attemptStartedAt = Date.now();
397
411
  lastAttemptedAccount = account;
@@ -402,10 +416,23 @@ export async function handleCodexResponsesRequest(ctx) {
402
416
  method: "POST",
403
417
  headers: buildCodexUpstreamHeaders(ctx.headers, account),
404
418
  body: bodyStr,
405
- signal: AbortSignal.timeout(CODEX_UPSTREAM_TIMEOUT_MS),
419
+ signal: ctx.abortSignal
420
+ ? AbortSignal.any([
421
+ ctx.abortSignal,
422
+ AbortSignal.timeout(CODEX_UPSTREAM_TIMEOUT_MS),
423
+ ])
424
+ : AbortSignal.timeout(CODEX_UPSTREAM_TIMEOUT_MS),
406
425
  });
407
426
  }
408
427
  catch (error) {
428
+ if (ctx.abortSignal?.aborted) {
429
+ writeAttempt(account, attempt, attemptStartedAt, 499, {
430
+ errorType: "client_cancelled",
431
+ errorMessage: "Client cancelled Codex request",
432
+ retryable: false,
433
+ });
434
+ return cancelRequest(account);
435
+ }
409
436
  // A transport failure message is derived from local state — resolved
410
437
  // hostnames, socket paths, Node internals — and says nothing the caller
411
438
  // can act on. Keep the detail in the log and return a fixed string, so
@@ -860,6 +860,7 @@ export type ProxyRollingState = {
860
860
  generation: number;
861
861
  }>;
862
862
  queuedSockets: number;
863
+ pendingTransfers?: number;
863
864
  rejectedSockets: number;
864
865
  failedTransfers: number;
865
866
  lastFailure: {
@@ -871,7 +872,7 @@ export type ProxyRollingState = {
871
872
  workerPid?: number;
872
873
  workerExitCode?: number | null;
873
874
  workerExitSignal?: string | null;
874
- supervisorAction?: "none" | "sigkill_after_transfer_failure";
875
+ supervisorAction?: "none" | "sigkill_after_transfer_failure" | "cancel_uncommitted_socket";
875
876
  } | null;
876
877
  };
877
878
  export type ProxySupervisorState = {