@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.
- 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 +8 -2
- package/dist/proxy/codexFallback.js +185 -1
- package/dist/proxy/proxyConfig.js +45 -2
- 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/proxy/routingPolicy.js +3 -0
- package/dist/server/routes/claudeProxyRoutes.js +240 -64
- package/dist/server/routes/codexProxyRoutes.js +33 -6
- package/dist/types/cli.d.ts +2 -1
- package/dist/types/codex.d.ts +10 -0
- package/dist/types/proxy.d.ts +11 -2
- package/dist/types/subscription.d.ts +3 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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,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
|
-
*
|
|
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
|
-
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
|
-
|
|
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
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
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
|