@juspay/neurolink 12.12.4 → 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.
- package/CHANGELOG.md +2 -2
- package/dist/auth/tokenStore.d.ts +9 -1
- package/dist/auth/tokenStore.js +29 -0
- package/dist/browser/neurolink.min.js +399 -398
- package/dist/cli/commands/proxy.js +65 -80
- package/dist/proxy/claudeFormat.d.ts +2 -2
- package/dist/proxy/claudeFormat.js +2 -2
- package/dist/proxy/codexFallback.d.ts +7 -1
- package/dist/proxy/codexFallback.js +181 -0
- package/dist/proxy/rollingProxyServer.js +10 -4
- package/dist/proxy/rollingWorkerProcess.js +8 -2
- package/dist/proxy/rollingWorkerProtocol.d.ts +2 -0
- package/dist/proxy/rollingWorkerProtocol.js +2 -0
- package/dist/proxy/rollingWorkerSupervisor.d.ts +5 -0
- package/dist/proxy/rollingWorkerSupervisor.js +75 -14
- package/dist/server/routes/claudeProxyRoutes.js +236 -61
- package/dist/server/routes/codexProxyRoutes.js +33 -6
- package/dist/types/cli.d.ts +2 -1
- package/dist/types/codex.d.ts +5 -0
- package/dist/types/proxy.d.ts +10 -2
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
this.
|
|
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:
|
|
435
|
-
? "
|
|
436
|
-
:
|
|
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 (
|
|
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
|
-
|
|
3205
|
-
|
|
3206
|
+
startupPrune ??= tokenStore
|
|
3207
|
+
.pruneExpired()
|
|
3208
|
+
.then(() => {
|
|
3209
|
+
startupPruneDone = true;
|
|
3210
|
+
})
|
|
3211
|
+
.finally(() => {
|
|
3212
|
+
startupPrune = undefined;
|
|
3213
|
+
});
|
|
3214
|
+
await startupPrune;
|
|
3206
3215
|
}
|
|
3207
|
-
const
|
|
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 (
|
|
3228
|
+
if (inventory[key].disabled) {
|
|
3219
3229
|
const existingState = getOrCreateRuntimeState(key);
|
|
3220
|
-
const disabledReason =
|
|
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 =
|
|
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
|
-
*
|
|
3627
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
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
|
|
138
|
-
|
|
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
|
|
145
|
-
if (
|
|
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 =
|
|
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:
|
|
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
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -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 = {
|