@juspay/neurolink 12.12.3 → 12.12.5

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,5 +1,7 @@
1
1
  import type { ProxyWorkerControlMessage, ProxyWorkerStatusMessage } from "../types/index.js";
2
2
  export declare const PROXY_SOCKET_WORKER_ENV = "NEUROLINK_PROXY_SOCKET_WORKER";
3
+ /** The worker has not been sent a commit and cannot have served this socket. */
4
+ export declare const PROXY_SOCKET_OFFER_TIMEOUT = "PROXY_SOCKET_OFFER_TIMEOUT";
3
5
  export declare const PROXY_ROLLING_SUPERVISOR_ENV = "NEUROLINK_PROXY_ROLLING_SUPERVISOR";
4
6
  export declare function isProxyWorkerControlMessage(value: unknown): value is ProxyWorkerControlMessage;
5
7
  export declare function isProxyWorkerStatusMessage(value: unknown): value is ProxyWorkerStatusMessage;
@@ -1,4 +1,6 @@
1
1
  export const PROXY_SOCKET_WORKER_ENV = "NEUROLINK_PROXY_SOCKET_WORKER";
2
+ /** The worker has not been sent a commit and cannot have served this socket. */
3
+ export const PROXY_SOCKET_OFFER_TIMEOUT = "PROXY_SOCKET_OFFER_TIMEOUT";
2
4
  export const PROXY_ROLLING_SUPERVISOR_ENV = "NEUROLINK_PROXY_ROLLING_SUPERVISOR";
3
5
  export function isProxyWorkerControlMessage(value) {
4
6
  if (!value || typeof value !== "object") {
@@ -11,6 +11,10 @@ export declare class RollingWorkerSupervisor {
11
11
  private candidate;
12
12
  private readonly draining;
13
13
  private readonly queuedSockets;
14
+ private flushingSockets;
15
+ private consecutiveOfferTimeouts;
16
+ private lastStallReplacementAt;
17
+ private transferStateTimer;
14
18
  private replacement;
15
19
  private rejectedSockets;
16
20
  private failedTransfers;
@@ -40,5 +44,6 @@ export declare class RollingWorkerSupervisor {
40
44
  private extractLifecycleFailureDetails;
41
45
  private recordFailure;
42
46
  private recordEvent;
47
+ private scheduleTransferState;
43
48
  private publishState;
44
49
  }
@@ -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
  }
@@ -35,6 +35,9 @@ export function buildProxyTranslationPlan(primary, fallbackChain, requestedModel
35
35
  attempts.push({
36
36
  provider: fallback.provider,
37
37
  model: fallback.model,
38
+ ...(fallback.reasoningEffort !== undefined
39
+ ? { reasoningEffort: fallback.reasoningEffort }
40
+ : {}),
38
41
  label: `${fallback.provider}/${fallback.model}`,
39
42
  });
40
43
  }
@@ -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,12 +3633,11 @@ 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
- const { ctx, body, model, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
3640
+ const { ctx, body, model, reasoningEffort, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
3632
3641
  const codexCtx = {
3633
3642
  ...ctx,
3634
3643
  requestId: `${ctx.requestId}:codex-fallback`,
@@ -3640,7 +3649,7 @@ async function executeClaudeCodexFallback(args) {
3640
3649
  },
3641
3650
  query: {},
3642
3651
  params: {},
3643
- body: convertClaudeRequestToCodex(body, model),
3652
+ body: convertClaudeRequestToCodex(body, model, reasoningEffort),
3644
3653
  metadata: { ...ctx.metadata, "neurolink.codexFallback": true },
3645
3654
  // Keep the child attribution isolated until its stream has passed
3646
3655
  // validation. A failed Codex attempt must not look like a served request.
@@ -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) {
@@ -3827,7 +3933,7 @@ async function tryConfiguredClaudeFallbackChain(args) {
3827
3933
  }
3828
3934
  const fallbackStart = Date.now();
3829
3935
  try {
3830
- logger.always(`[proxy] fallback → ${fallback.provider}/${fallback.model}`);
3936
+ logger.always(`[proxy] fallback → ${fallback.provider}/${fallback.model}${fallback.reasoningEffort ? ` reasoning=${fallback.reasoningEffort}` : ""}`);
3831
3937
  let response;
3832
3938
  if (fallback.provider === "codex") {
3833
3939
  // Codex is a local OAuth account pool, not a generic SDK provider.
@@ -3836,6 +3942,7 @@ async function tryConfiguredClaudeFallbackChain(args) {
3836
3942
  ctx,
3837
3943
  body,
3838
3944
  model: fallback.model,
3945
+ reasoningEffort: fallback.reasoningEffort,
3839
3946
  tracer,
3840
3947
  requestStartTime,
3841
3948
  logProxyBody,
@@ -3872,6 +3979,9 @@ async function tryConfiguredClaudeFallbackChain(args) {
3872
3979
  providerLabel: fallback.provider,
3873
3980
  });
3874
3981
  }
3982
+ if (fallback.provider === "codex" && body.stream) {
3983
+ return { response };
3984
+ }
3875
3985
  recordFallbackAttempt({
3876
3986
  provider: fallback.provider,
3877
3987
  model: fallback.model,
@@ -3898,7 +4008,27 @@ async function tryConfiguredClaudeFallbackChain(args) {
3898
4008
  return { response };
3899
4009
  }
3900
4010
  catch (fallbackErr) {
3901
- 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 =
3902
4032
  getCodexFallbackInvalidRequestFailure(fallbackErr) ?? undefined;
3903
4033
  const errMsg = redactProviderErrorMessage(fallbackErr instanceof Error
3904
4034
  ? fallbackErr.message
@@ -3933,14 +4063,29 @@ async function tryConfiguredClaudeFallbackChain(args) {
3933
4063
  durationMs: Date.now() - fallbackStart,
3934
4064
  });
3935
4065
  lastFallbackError = `[${fallback.provider}/${fallback.model}] ${redactProviderErrorMessage(describeTransportError(fallbackErr))}`;
4066
+ if (ctx.abortSignal?.aborted) {
4067
+ break;
4068
+ }
3936
4069
  }
3937
4070
  }
3938
4071
  return {
3939
4072
  response: null,
3940
4073
  lastErrorMessage: lastFallbackError,
4074
+ terminalFailure,
3941
4075
  ...(invalidRequestFailure ? { invalidRequestFailure } : {}),
3942
4076
  };
3943
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
+ }
3944
4089
  async function tryAutoClaudeFallback(args) {
3945
4090
  const { ctx, body, tracer, requestStartTime, logProxyBody, logFinalRequest } = args;
3946
4091
  const fallbackStart = Date.now();
@@ -6324,6 +6469,15 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6324
6469
  logFinalRequest,
6325
6470
  });
6326
6471
  }
6472
+ if (configuredFallbackResult.terminalFailure) {
6473
+ const failure = configuredFallbackResult.terminalFailure;
6474
+ return buildConfiguredClaudeFallbackFailure({
6475
+ failure,
6476
+ buildLoggedClaudeError,
6477
+ tracer,
6478
+ requestStartTime,
6479
+ });
6480
+ }
6327
6481
  return buildDeferredClaudeAccountFailureResponse({
6328
6482
  ctx,
6329
6483
  tracer,
@@ -6820,13 +6974,25 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6820
6974
  if (configuredFallbackResult.response) {
6821
6975
  return configuredFallbackResult.response;
6822
6976
  }
6823
- if (configuredFallbackResult.invalidRequestFailure &&
6824
- !loopState.sawRateLimit) {
6825
- // A converted Codex request can be rejected independently of the original
6826
- // Anthropic request. Preserve a real pool 429 as the actionable terminal
6827
- // response when both occurred.
6828
- loopState.invalidRequestFailure =
6829
- 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
+ });
6830
6996
  }
6831
6997
  fallbackFailureMessage = configuredFallbackResult.lastErrorMessage;
6832
6998
  // A translation-layer-selected provider is only permitted by an explicit
@@ -6848,6 +7014,16 @@ async function handleAnthropicRoutedClaudeRequest(args) {
6848
7014
  fallbackFailureMessage =
6849
7015
  autoFallbackResult.lastErrorMessage ?? fallbackFailureMessage;
6850
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
+ }
6851
7027
  loopState.fallbackFailureMessage = fallbackFailureMessage;
6852
7028
  }
6853
7029
  // Terminal failure — usually "every account is rate-limited". This is the