@juspay/neurolink 12.7.1 → 12.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +407 -399
- package/dist/cli/commands/proxy.js +40 -19
- package/dist/localUsage/scanWindow.d.ts +34 -5
- package/dist/localUsage/scanWindow.js +34 -5
- package/dist/proxy/codexAccountUsage.d.ts +5 -0
- package/dist/proxy/codexAccountUsage.js +18 -0
- package/dist/proxy/codexFallback.d.ts +26 -0
- package/dist/proxy/codexFallback.js +371 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +23 -1
- package/dist/server/routes/claudeProxyRoutes.js +351 -96
- package/dist/server/routes/codexProxyRoutes.d.ts +3 -1
- package/dist/server/routes/codexProxyRoutes.js +1 -1
- package/dist/types/claudeProxy.d.ts +16 -0
- package/dist/types/claudeProxy.js +4 -0
- package/dist/types/codex.d.ts +62 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ import { ProviderTransportCoordinator } from "../../proxy/providerTransportCoord
|
|
|
23
23
|
import { MAX_COOLDOWN_MS_BY_REASON } from "../../proxy/routingEvidence.js";
|
|
24
24
|
import { buildProxyLimitHeaders, summarizePoolHeadroom, } from "../../proxy/quotaHeaders.js";
|
|
25
25
|
import { buildClaudeError, ClaudeStreamSerializer, generateToolUseId, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
|
|
26
|
+
import { CodexFallbackResponseError, consumeCodexFallbackResponse, convertClaudeRequestToCodex, } from "../../proxy/codexFallback.js";
|
|
26
27
|
import { buildAnthropicModelsListResponse, buildTranslationOptions, extractText, extractToolArgs, extractUsageFromStreamResult, handleTranslatedJsonRequest, handleTranslatedStreamRequest, hasTranslatedOutput, } from "../../proxy/proxyTranslationEngine.js";
|
|
27
28
|
import { tracers } from "../../telemetry/tracers.js";
|
|
28
29
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
@@ -56,6 +57,7 @@ import { sanitizeForLog } from "../../utils/logSanitize.js";
|
|
|
56
57
|
import { logger } from "../../utils/logger.js";
|
|
57
58
|
import { raceWithAbort, withTimeout } from "../../utils/async/withTimeout.js";
|
|
58
59
|
import { ProviderHealthChecker } from "../../utils/providerHealth.js";
|
|
60
|
+
import { handleCodexResponsesRequest } from "./codexProxyRoutes.js";
|
|
59
61
|
// ---------------------------------------------------------------------------
|
|
60
62
|
// Helpers
|
|
61
63
|
// ---------------------------------------------------------------------------
|
|
@@ -2535,20 +2537,14 @@ async function applyShareAccountGates(args) {
|
|
|
2535
2537
|
grant,
|
|
2536
2538
|
retryAfterSeconds: earliestShareRecoverySeconds(views, now),
|
|
2537
2539
|
});
|
|
2538
|
-
// Assigned, not merged into an existing object: a refusal is often the first
|
|
2539
|
-
// thing to touch this context, and only copying when headers already existed
|
|
2540
|
-
// meant the borrower learned nothing about why it was refused.
|
|
2541
|
-
args.ctx.responseHeaders = {
|
|
2542
|
-
...(args.ctx.responseHeaders ?? {}),
|
|
2543
|
-
...refusal.headers,
|
|
2544
|
-
};
|
|
2545
2540
|
logger.always(`[proxy] share ${share.peerLabel} withheld: ${reason} (${decision.excluded.length} accounts)`);
|
|
2546
2541
|
return {
|
|
2547
2542
|
accounts: [],
|
|
2548
2543
|
refusal: {
|
|
2549
|
-
response: args.buildLoggedClaudeError(refusal.status, refusal.body.error.message, refusal.body.error.type),
|
|
2550
2544
|
status: refusal.status,
|
|
2551
2545
|
message: refusal.body.error.message,
|
|
2546
|
+
errorType: refusal.body.error.type,
|
|
2547
|
+
responseHeaders: refusal.headers,
|
|
2552
2548
|
},
|
|
2553
2549
|
};
|
|
2554
2550
|
}
|
|
@@ -3009,7 +3005,7 @@ function earliestShareRecoverySeconds(views, now) {
|
|
|
3009
3005
|
return Math.max(1, Math.round((Math.min(...resets) - now) / 1000));
|
|
3010
3006
|
}
|
|
3011
3007
|
async function loadClaudeProxyAccounts(args) {
|
|
3012
|
-
const { ctx, body,
|
|
3008
|
+
const { ctx, body, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), setRoutingDecision, } = args;
|
|
3013
3009
|
const fs = await import("fs");
|
|
3014
3010
|
const os = await import("os");
|
|
3015
3011
|
const accounts = [];
|
|
@@ -3176,10 +3172,12 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
3176
3172
|
message: "OAuth authentication is not allowed for this organization.",
|
|
3177
3173
|
errorCode: "oauth_not_allowed_for_organization",
|
|
3178
3174
|
});
|
|
3179
|
-
tracer?.setError("permission_error", entitlementMessage);
|
|
3180
|
-
tracer?.end(403, Date.now() - requestStartTime);
|
|
3181
3175
|
return {
|
|
3182
|
-
|
|
3176
|
+
failure: {
|
|
3177
|
+
status: 403,
|
|
3178
|
+
message: entitlementMessage,
|
|
3179
|
+
errorType: "permission_error",
|
|
3180
|
+
},
|
|
3183
3181
|
};
|
|
3184
3182
|
}
|
|
3185
3183
|
const noCredentialsMessage = accountAllowlist
|
|
@@ -3187,10 +3185,12 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
3187
3185
|
: compoundKeys.length > 0
|
|
3188
3186
|
? "Configured Anthropic accounts are disabled or unavailable"
|
|
3189
3187
|
: "No Anthropic credentials found";
|
|
3190
|
-
tracer?.setError("authentication_error", noCredentialsMessage);
|
|
3191
|
-
tracer?.end(401, Date.now() - requestStartTime);
|
|
3192
3188
|
return {
|
|
3193
|
-
|
|
3189
|
+
failure: {
|
|
3190
|
+
status: 401,
|
|
3191
|
+
message: noCredentialsMessage,
|
|
3192
|
+
errorType: "authentication_error",
|
|
3193
|
+
},
|
|
3194
3194
|
};
|
|
3195
3195
|
}
|
|
3196
3196
|
for (const account of accounts) {
|
|
@@ -3228,28 +3228,30 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
3228
3228
|
`Ask the lender to widen the share, or use a model it allows.`
|
|
3229
3229
|
: `Borrowed account(s) are no longer covered by a lease: ${detail}. ` +
|
|
3230
3230
|
`Run 'neurolink proxy peer sync' to check in with the lender, or ask them to resume the share.`;
|
|
3231
|
-
tracer?.setError("permission_error", leaseMessage);
|
|
3232
|
-
tracer?.end(403, Date.now() - requestStartTime);
|
|
3233
3231
|
return {
|
|
3234
|
-
|
|
3232
|
+
failure: {
|
|
3233
|
+
status: 403,
|
|
3234
|
+
message: leaseMessage,
|
|
3235
|
+
errorType: "permission_error",
|
|
3236
|
+
},
|
|
3235
3237
|
};
|
|
3236
3238
|
}
|
|
3237
3239
|
const shareFiltered = await applyShareAccountGates({
|
|
3238
3240
|
accounts: leasedAccounts,
|
|
3239
|
-
ctx,
|
|
3240
|
-
buildLoggedClaudeError,
|
|
3241
3241
|
});
|
|
3242
3242
|
if (shareFiltered.refusal) {
|
|
3243
|
-
|
|
3244
|
-
tracer?.end(shareFiltered.refusal.status, Date.now() - requestStartTime);
|
|
3245
|
-
return { response: shareFiltered.refusal.response };
|
|
3243
|
+
return { failure: shareFiltered.refusal };
|
|
3246
3244
|
}
|
|
3247
3245
|
const enabledAccounts = shareFiltered.accounts;
|
|
3248
3246
|
if (enabledAccounts.length === 0) {
|
|
3249
3247
|
const reauthMsg = formatReauthMessage(accounts.map((account) => account.label));
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3248
|
+
return {
|
|
3249
|
+
failure: {
|
|
3250
|
+
status: 401,
|
|
3251
|
+
message: reauthMsg,
|
|
3252
|
+
errorType: "authentication_error",
|
|
3253
|
+
},
|
|
3254
|
+
};
|
|
3253
3255
|
}
|
|
3254
3256
|
const { orderedAccounts, metricsByKey } = selectClaudeProxyAccountOrder({
|
|
3255
3257
|
enabledAccounts,
|
|
@@ -3430,6 +3432,121 @@ async function executeClaudeFallbackWithRetry(args) {
|
|
|
3430
3432
|
}
|
|
3431
3433
|
throw lastError;
|
|
3432
3434
|
}
|
|
3435
|
+
/**
|
|
3436
|
+
* Run the configured `codex` fallback through the native pooled Codex route.
|
|
3437
|
+
*
|
|
3438
|
+
* The inner response is fully buffered and validated before this function
|
|
3439
|
+
* creates a single Claude frame. That preserves the proxy's no-replay-after-
|
|
3440
|
+
* output guarantee when Codex returns an incomplete stream.
|
|
3441
|
+
*/
|
|
3442
|
+
async function executeClaudeCodexFallback(args) {
|
|
3443
|
+
const { ctx, body, model, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
|
|
3444
|
+
const codexCtx = {
|
|
3445
|
+
...ctx,
|
|
3446
|
+
requestId: `${ctx.requestId}:codex-fallback`,
|
|
3447
|
+
method: "POST",
|
|
3448
|
+
path: "/backend-api/codex/responses",
|
|
3449
|
+
headers: {
|
|
3450
|
+
"content-type": "application/json",
|
|
3451
|
+
accept: "text/event-stream",
|
|
3452
|
+
},
|
|
3453
|
+
query: {},
|
|
3454
|
+
params: {},
|
|
3455
|
+
body: convertClaudeRequestToCodex(body, model),
|
|
3456
|
+
// Keep the child attribution isolated until its stream has passed
|
|
3457
|
+
// validation. A failed Codex attempt must not look like a served request.
|
|
3458
|
+
responseHeaders: {},
|
|
3459
|
+
};
|
|
3460
|
+
const codexResponse = await handleCodexResponsesRequest(codexCtx);
|
|
3461
|
+
const codexHeaders = { ...(codexCtx.responseHeaders ?? {}) };
|
|
3462
|
+
let parsed;
|
|
3463
|
+
try {
|
|
3464
|
+
parsed = await consumeCodexFallbackResponse(codexResponse);
|
|
3465
|
+
}
|
|
3466
|
+
catch (error) {
|
|
3467
|
+
if (error instanceof CodexFallbackResponseError) {
|
|
3468
|
+
logger.always(`[proxy] Codex fallback returned ${error.status}: ${sanitizeForLog(error.responseBody, 500)}`);
|
|
3469
|
+
}
|
|
3470
|
+
throw error;
|
|
3471
|
+
}
|
|
3472
|
+
if (Object.keys(codexHeaders).length > 0) {
|
|
3473
|
+
ctx.responseHeaders ??= {};
|
|
3474
|
+
Object.assign(ctx.responseHeaders, redactHeadersForBorrower(codexHeaders));
|
|
3475
|
+
}
|
|
3476
|
+
const accountLabel = codexHeaders["x-neurolink-account"] ?? "";
|
|
3477
|
+
const accountType = codexHeaders["x-neurolink-account-type"] ?? "codex-oauth";
|
|
3478
|
+
const internal = {
|
|
3479
|
+
content: parsed.text,
|
|
3480
|
+
// Keep the original Anthropic model in the client wire response. The
|
|
3481
|
+
// attribution headers above expose that Codex served the fallback.
|
|
3482
|
+
model: body.model,
|
|
3483
|
+
finishReason: parsed.finishReason,
|
|
3484
|
+
...(parsed.usage ? { usage: parsed.usage } : {}),
|
|
3485
|
+
toolCalls: parsed.toolCalls,
|
|
3486
|
+
};
|
|
3487
|
+
if (body.stream) {
|
|
3488
|
+
const serializer = new ClaudeStreamSerializer(body.model, parsed.usage?.input ?? 0);
|
|
3489
|
+
const frames = [];
|
|
3490
|
+
for (const frame of serializer.start()) {
|
|
3491
|
+
frames.push(frame);
|
|
3492
|
+
}
|
|
3493
|
+
if (parsed.text) {
|
|
3494
|
+
for (const frame of serializer.pushDelta(parsed.text)) {
|
|
3495
|
+
frames.push(frame);
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
for (const toolCall of parsed.toolCalls) {
|
|
3499
|
+
for (const frame of serializer.pushToolUse(generateToolUseId(), toolCall.toolName, toolCall.args)) {
|
|
3500
|
+
frames.push(frame);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
for (const frame of serializer.finish(parsed.usage?.output, parsed.finishReason)) {
|
|
3504
|
+
frames.push(frame);
|
|
3505
|
+
}
|
|
3506
|
+
tracer?.end(200, Date.now() - requestStartTime);
|
|
3507
|
+
logFinalRequest(200, accountLabel, accountType, undefined, undefined, {
|
|
3508
|
+
inputTokens: parsed.usage?.input,
|
|
3509
|
+
outputTokens: parsed.usage?.output,
|
|
3510
|
+
cacheCreationTokens: parsed.usage?.cacheCreationTokens,
|
|
3511
|
+
cacheReadTokens: parsed.usage?.cacheReadTokens,
|
|
3512
|
+
});
|
|
3513
|
+
const bufferedBody = frames.join("");
|
|
3514
|
+
logProxyBody({
|
|
3515
|
+
phase: "client_response",
|
|
3516
|
+
headers: { "content-type": "text/event-stream" },
|
|
3517
|
+
body: bufferedBody,
|
|
3518
|
+
bodySize: Buffer.byteLength(bufferedBody, "utf8"),
|
|
3519
|
+
contentType: "text/event-stream",
|
|
3520
|
+
responseStatus: 200,
|
|
3521
|
+
durationMs: Date.now() - requestStartTime,
|
|
3522
|
+
});
|
|
3523
|
+
async function* sseGenerator() {
|
|
3524
|
+
for (const frame of frames) {
|
|
3525
|
+
yield frame;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
return sseGenerator();
|
|
3529
|
+
}
|
|
3530
|
+
tracer?.end(200, Date.now() - requestStartTime);
|
|
3531
|
+
const clientResponse = serializeClaudeResponse(internal, body.model);
|
|
3532
|
+
logFinalRequest(200, accountLabel, accountType, undefined, undefined, {
|
|
3533
|
+
inputTokens: parsed.usage?.input,
|
|
3534
|
+
outputTokens: parsed.usage?.output,
|
|
3535
|
+
cacheCreationTokens: parsed.usage?.cacheCreationTokens,
|
|
3536
|
+
cacheReadTokens: parsed.usage?.cacheReadTokens,
|
|
3537
|
+
});
|
|
3538
|
+
const clientResponseText = JSON.stringify(clientResponse);
|
|
3539
|
+
logProxyBody({
|
|
3540
|
+
phase: "client_response",
|
|
3541
|
+
headers: { "content-type": "application/json" },
|
|
3542
|
+
body: clientResponseText,
|
|
3543
|
+
bodySize: Buffer.byteLength(clientResponseText, "utf8"),
|
|
3544
|
+
contentType: "application/json",
|
|
3545
|
+
responseStatus: 200,
|
|
3546
|
+
durationMs: Date.now() - requestStartTime,
|
|
3547
|
+
});
|
|
3548
|
+
return clientResponse;
|
|
3549
|
+
}
|
|
3433
3550
|
/**
|
|
3434
3551
|
* Try each borrowable peer in priority order once the local pool is spent.
|
|
3435
3552
|
*
|
|
@@ -3486,10 +3603,20 @@ async function tryBorrowFromPeers(args) {
|
|
|
3486
3603
|
}
|
|
3487
3604
|
return null;
|
|
3488
3605
|
}
|
|
3606
|
+
function getCodexFallbackInvalidRequestFailure(error) {
|
|
3607
|
+
if (!(error instanceof CodexFallbackResponseError) || error.status !== 400) {
|
|
3608
|
+
return null;
|
|
3609
|
+
}
|
|
3610
|
+
return {
|
|
3611
|
+
status: error.status,
|
|
3612
|
+
body: error.responseBody,
|
|
3613
|
+
contentType: "application/json",
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3489
3616
|
async function tryConfiguredClaudeFallbackChain(args) {
|
|
3490
|
-
const { ctx, body, parsedFallbackRequest, modelRouter, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
|
|
3491
|
-
const
|
|
3492
|
-
|
|
3617
|
+
const { ctx, body, parsedFallbackRequest, fallbackPlan: providedFallbackPlan, modelRouter, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
|
|
3618
|
+
const fallbackPlan = providedFallbackPlan ??
|
|
3619
|
+
buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, modelRouter?.getFallbackChain() ?? [], body.model, parsedFallbackRequest);
|
|
3493
3620
|
logProxyBody({
|
|
3494
3621
|
phase: "routing_decision",
|
|
3495
3622
|
contentType: "application/json",
|
|
@@ -3504,41 +3631,58 @@ async function tryConfiguredClaudeFallbackChain(args) {
|
|
|
3504
3631
|
reason: "all_anthropic_accounts_exhausted",
|
|
3505
3632
|
});
|
|
3506
3633
|
let lastFallbackError;
|
|
3634
|
+
let invalidRequestFailure;
|
|
3507
3635
|
for (const fallback of fallbackPlan.attempts.slice(1)) {
|
|
3508
3636
|
if (!fallback.provider || !fallback.model) {
|
|
3509
3637
|
continue;
|
|
3510
3638
|
}
|
|
3511
|
-
const availability = await ProviderHealthChecker.checkFallbackProviderAvailability(fallback.provider, fallback.model);
|
|
3512
|
-
if (!availability.available) {
|
|
3513
|
-
const reason = availability.reason ?? "provider unavailable";
|
|
3514
|
-
logger.always(`[proxy] fallback ${fallback.provider}/${fallback.model} health-check failed (${reason}), skipping`);
|
|
3515
|
-
recordFallbackAttempt({
|
|
3516
|
-
provider: fallback.provider,
|
|
3517
|
-
model: fallback.model,
|
|
3518
|
-
status: "failure",
|
|
3519
|
-
errorMessage: `[unavailable] ${reason}`,
|
|
3520
|
-
durationMs: 0,
|
|
3521
|
-
});
|
|
3522
|
-
lastFallbackError = `[${fallback.provider}/${fallback.model}] unavailable: ${reason}`;
|
|
3523
|
-
continue;
|
|
3524
|
-
}
|
|
3525
3639
|
const fallbackStart = Date.now();
|
|
3526
3640
|
try {
|
|
3527
3641
|
logger.always(`[proxy] fallback → ${fallback.provider}/${fallback.model}`);
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
}
|
|
3642
|
+
let response;
|
|
3643
|
+
if (fallback.provider === "codex") {
|
|
3644
|
+
// Codex is a local OAuth account pool, not a generic SDK provider.
|
|
3645
|
+
// Calling its native route preserves account rotation and cooldowns.
|
|
3646
|
+
response = await executeClaudeCodexFallback({
|
|
3647
|
+
ctx,
|
|
3648
|
+
body,
|
|
3649
|
+
model: fallback.model,
|
|
3650
|
+
tracer,
|
|
3651
|
+
requestStartTime,
|
|
3652
|
+
logProxyBody,
|
|
3653
|
+
logFinalRequest,
|
|
3654
|
+
});
|
|
3655
|
+
}
|
|
3656
|
+
else {
|
|
3657
|
+
const availability = await ProviderHealthChecker.checkFallbackProviderAvailability(fallback.provider, fallback.model);
|
|
3658
|
+
if (!availability.available) {
|
|
3659
|
+
const reason = availability.reason ?? "provider unavailable";
|
|
3660
|
+
logger.always(`[proxy] fallback ${fallback.provider}/${fallback.model} health-check failed (${reason}), skipping`);
|
|
3661
|
+
recordFallbackAttempt({
|
|
3662
|
+
provider: fallback.provider,
|
|
3663
|
+
model: fallback.model,
|
|
3664
|
+
status: "failure",
|
|
3665
|
+
errorMessage: `[unavailable] ${reason}`,
|
|
3666
|
+
durationMs: 0,
|
|
3667
|
+
});
|
|
3668
|
+
lastFallbackError = `[${fallback.provider}/${fallback.model}] unavailable: ${reason}`;
|
|
3669
|
+
continue;
|
|
3670
|
+
}
|
|
3671
|
+
const options = buildProxyFallbackOptions(parsedFallbackRequest, {
|
|
3672
|
+
provider: fallback.provider,
|
|
3673
|
+
model: fallback.model,
|
|
3674
|
+
});
|
|
3675
|
+
response = await executeClaudeFallbackWithRetry({
|
|
3676
|
+
ctx,
|
|
3677
|
+
body,
|
|
3678
|
+
tracer,
|
|
3679
|
+
requestStartTime,
|
|
3680
|
+
logProxyBody,
|
|
3681
|
+
logFinalRequest,
|
|
3682
|
+
options: options,
|
|
3683
|
+
providerLabel: fallback.provider,
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3542
3686
|
recordFallbackAttempt({
|
|
3543
3687
|
provider: fallback.provider,
|
|
3544
3688
|
model: fallback.model,
|
|
@@ -3552,17 +3696,21 @@ async function tryConfiguredClaudeFallbackChain(args) {
|
|
|
3552
3696
|
attemptCount: fallbackPlan.attempts.slice(1).length,
|
|
3553
3697
|
reason: "fallback_success",
|
|
3554
3698
|
});
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3699
|
+
if (fallback.provider !== "codex") {
|
|
3700
|
+
// A different provider produced this response — say so, and report no
|
|
3701
|
+
// quota. Emitting the last Anthropic snapshot here would attribute one
|
|
3702
|
+
// provider's capacity to another's output.
|
|
3703
|
+
publishLimitHeaders(ctx, {
|
|
3704
|
+
quota: null,
|
|
3705
|
+
source: "none",
|
|
3706
|
+
servedBy: fallback.provider,
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3563
3709
|
return { response };
|
|
3564
3710
|
}
|
|
3565
3711
|
catch (fallbackErr) {
|
|
3712
|
+
invalidRequestFailure ??=
|
|
3713
|
+
getCodexFallbackInvalidRequestFailure(fallbackErr) ?? undefined;
|
|
3566
3714
|
const errMsg = redactProviderErrorMessage(fallbackErr instanceof Error
|
|
3567
3715
|
? fallbackErr.message
|
|
3568
3716
|
: String(fallbackErr));
|
|
@@ -3598,7 +3746,11 @@ async function tryConfiguredClaudeFallbackChain(args) {
|
|
|
3598
3746
|
lastFallbackError = `[${fallback.provider}/${fallback.model}] ${redactProviderErrorMessage(describeTransportError(fallbackErr))}`;
|
|
3599
3747
|
}
|
|
3600
3748
|
}
|
|
3601
|
-
return {
|
|
3749
|
+
return {
|
|
3750
|
+
response: null,
|
|
3751
|
+
lastErrorMessage: lastFallbackError,
|
|
3752
|
+
...(invalidRequestFailure ? { invalidRequestFailure } : {}),
|
|
3753
|
+
};
|
|
3602
3754
|
}
|
|
3603
3755
|
async function tryAutoClaudeFallback(args) {
|
|
3604
3756
|
const { ctx, body, tracer, requestStartTime, logProxyBody, logFinalRequest } = args;
|
|
@@ -3769,28 +3921,32 @@ function buildClaudeAnthropicFailureResponse(args) {
|
|
|
3769
3921
|
tracer?.end(503, Date.now() - requestStartTime);
|
|
3770
3922
|
return buildLoggedClaudeError(503, authCooldownMessage, "token_refresh_unavailable");
|
|
3771
3923
|
}
|
|
3772
|
-
if (invalidRequestFailure) {
|
|
3773
|
-
|
|
3924
|
+
if (invalidRequestFailure && !sawRateLimit) {
|
|
3925
|
+
const parsedUpstream = parseClaudeErrorBody(invalidRequestFailure.body);
|
|
3926
|
+
const preserveUpstreamBody = parsedUpstream.message !== undefined;
|
|
3927
|
+
const message = summarizeErrorMessage(parsedUpstream.message ?? invalidRequestFailure.body);
|
|
3928
|
+
const errorBodyText = preserveUpstreamBody
|
|
3929
|
+
? invalidRequestFailure.body
|
|
3930
|
+
: JSON.stringify(buildClaudeError(invalidRequestFailure.status, message, "invalid_request_error"));
|
|
3931
|
+
const contentType = preserveUpstreamBody
|
|
3932
|
+
? (invalidRequestFailure.contentType ?? "application/json")
|
|
3933
|
+
: "application/json";
|
|
3934
|
+
tracer?.setError("invalid_request_error", message);
|
|
3774
3935
|
tracer?.end(invalidRequestFailure.status, Date.now() - requestStartTime);
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
return parsedError;
|
|
3790
|
-
}
|
|
3791
|
-
catch {
|
|
3792
|
-
return buildLoggedClaudeError(invalidRequestFailure.status, summarizeErrorMessage(invalidRequestFailure.body), "invalid_request_error");
|
|
3793
|
-
}
|
|
3936
|
+
logFinalRequest(invalidRequestFailure.status, "", "final", "invalid_request_error", message);
|
|
3937
|
+
logProxyBody({
|
|
3938
|
+
phase: "client_response",
|
|
3939
|
+
headers: { "content-type": contentType },
|
|
3940
|
+
body: errorBodyText,
|
|
3941
|
+
bodySize: Buffer.byteLength(errorBodyText, "utf8"),
|
|
3942
|
+
contentType,
|
|
3943
|
+
responseStatus: invalidRequestFailure.status,
|
|
3944
|
+
durationMs: Date.now() - requestStartTime,
|
|
3945
|
+
});
|
|
3946
|
+
return new Response(errorBodyText, {
|
|
3947
|
+
status: invalidRequestFailure.status,
|
|
3948
|
+
headers: { "content-type": contentType },
|
|
3949
|
+
});
|
|
3794
3950
|
}
|
|
3795
3951
|
if ((sawNetworkError || sawTransientFailure) && !sawRateLimit) {
|
|
3796
3952
|
const fallbackSuffix = fallbackFailureMessage
|
|
@@ -5085,7 +5241,7 @@ function finalizeAnthropicTerminalTransportError(args) {
|
|
|
5085
5241
|
return clientError;
|
|
5086
5242
|
}
|
|
5087
5243
|
async function handleAnthropicNonOkResponse(args) {
|
|
5088
|
-
const { response, account, accountState, enabledAccounts, orderedAccounts, tracer, requestStartTime, fetchStartMs, attemptNumber, logAttempt, logProxyBody, logFinalRequest, lastError, authFailureMessage, sawTransientFailure, invalidRequestFailure, entitlementFailure, } = args;
|
|
5244
|
+
const { response, account, accountState, enabledAccounts, orderedAccounts, tracer, requestStartTime, fetchStartMs, attemptNumber, logAttempt, logProxyBody, logFinalRequest, lastError, authFailureMessage, sawTransientFailure, invalidRequestFailure, entitlementFailure, allowConfiguredModelFallback = false, } = args;
|
|
5089
5245
|
let currentLastError = lastError;
|
|
5090
5246
|
let currentAuthFailureMessage = authFailureMessage;
|
|
5091
5247
|
let currentSawTransientFailure = sawTransientFailure;
|
|
@@ -5257,6 +5413,26 @@ async function handleAnthropicNonOkResponse(args) {
|
|
|
5257
5413
|
};
|
|
5258
5414
|
}
|
|
5259
5415
|
if (response.status === 404) {
|
|
5416
|
+
if (allowConfiguredModelFallback &&
|
|
5417
|
+
isAnthropicModelNotFound(response.status, errBody)) {
|
|
5418
|
+
// An upstream model retirement is provider-wide, not an account failure.
|
|
5419
|
+
// Do not cool or disable an account; leave the configured translation
|
|
5420
|
+
// fallback eligible so legacy Anthropic aliases can use the Codex target.
|
|
5421
|
+
logger.always(`[proxy] ← 404 account=${account.label} model unavailable; trying configured fallback`);
|
|
5422
|
+
logAttempt(404, "not_found_error", summarizeErrorMessage(errBody));
|
|
5423
|
+
tracer?.setError("not_found_error", summarizeErrorMessage(errBody));
|
|
5424
|
+
tracer?.recordRetry(account.label, "model_not_found");
|
|
5425
|
+
currentLastError = summarizeErrorMessage(errBody);
|
|
5426
|
+
return {
|
|
5427
|
+
continueLoop: false,
|
|
5428
|
+
lastError: currentLastError,
|
|
5429
|
+
authFailureMessage: currentAuthFailureMessage,
|
|
5430
|
+
sawTransientFailure: currentSawTransientFailure,
|
|
5431
|
+
invalidRequestFailure: currentInvalidRequestFailure,
|
|
5432
|
+
entitlementFailure: currentEntitlementFailure,
|
|
5433
|
+
upstreamSpan: undefined,
|
|
5434
|
+
};
|
|
5435
|
+
}
|
|
5260
5436
|
logger.always(`[proxy] ← 404 account=${account.label}`);
|
|
5261
5437
|
logAttempt(404, "not_found_error", summarizeErrorMessage(errBody));
|
|
5262
5438
|
tracer?.setError("not_found_error", summarizeErrorMessage(errBody));
|
|
@@ -5862,33 +6038,85 @@ async function fetchAnthropicAccountResponse(args) {
|
|
|
5862
6038
|
function shouldAttemptClaudeFallback(loopState) {
|
|
5863
6039
|
return loopState.invalidRequestFailure === null;
|
|
5864
6040
|
}
|
|
6041
|
+
function buildDeferredClaudeAccountFailureResponse(args) {
|
|
6042
|
+
const { ctx, tracer, requestStartTime, failure, buildLoggedClaudeError } = args;
|
|
6043
|
+
if (failure.responseHeaders) {
|
|
6044
|
+
ctx.responseHeaders = {
|
|
6045
|
+
...(ctx.responseHeaders ?? {}),
|
|
6046
|
+
...failure.responseHeaders,
|
|
6047
|
+
};
|
|
6048
|
+
}
|
|
6049
|
+
tracer?.setError(failure.errorType, failure.message);
|
|
6050
|
+
tracer?.end(failure.status, Date.now() - requestStartTime);
|
|
6051
|
+
return buildLoggedClaudeError(failure.status, failure.message, failure.errorType);
|
|
6052
|
+
}
|
|
5865
6053
|
async function handleAnthropicRoutedClaudeRequest(args) {
|
|
5866
6054
|
const { ctx, body, modelRouter, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), buildLoggedClaudeError, logProxyBody, logFinalRequest, setRoutingDecision, } = args;
|
|
5867
6055
|
const parsedRequest = parseClaudeRequest(body);
|
|
6056
|
+
const configuredFallbackPlan = buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, modelRouter?.getFallbackChain() ?? [], body.model, parsedRequest);
|
|
6057
|
+
const hasConfiguredFallback = configuredFallbackPlan.attempts
|
|
6058
|
+
.slice(1)
|
|
6059
|
+
.some((attempt) => Boolean(attempt.provider && attempt.model));
|
|
5868
6060
|
const loadedAccounts = await loadClaudeProxyAccounts({
|
|
5869
6061
|
ctx,
|
|
5870
6062
|
body,
|
|
5871
|
-
tracer,
|
|
5872
|
-
requestStartTime,
|
|
5873
6063
|
accountStrategy,
|
|
5874
6064
|
primaryAccountKey,
|
|
5875
6065
|
accountAllowlist,
|
|
5876
6066
|
quotaRoutingEnabled,
|
|
5877
6067
|
sessionSoftLimit,
|
|
5878
6068
|
sessionResetToleranceMs,
|
|
5879
|
-
buildLoggedClaudeError,
|
|
5880
6069
|
setRoutingDecision,
|
|
5881
6070
|
});
|
|
5882
|
-
if ("
|
|
6071
|
+
if ("failure" in loadedAccounts) {
|
|
5883
6072
|
// No usable local account. A node that has none of its own — or whose only
|
|
5884
6073
|
// accounts are disabled — is still entitled to borrow: that is the whole
|
|
5885
|
-
// point of being lent capacity. Peers
|
|
5886
|
-
//
|
|
6074
|
+
// point of being lent capacity. Peers and explicitly configured fallbacks
|
|
6075
|
+
// are tried before returning the credential error.
|
|
5887
6076
|
const peerOnlyResult = await tryBorrowFromPeers({ body, logFinalRequest });
|
|
5888
6077
|
if (peerOnlyResult) {
|
|
5889
6078
|
return peerOnlyResult;
|
|
5890
6079
|
}
|
|
5891
|
-
|
|
6080
|
+
const configuredFallbackResult = await tryConfiguredClaudeFallbackChain({
|
|
6081
|
+
ctx,
|
|
6082
|
+
body,
|
|
6083
|
+
parsedFallbackRequest: parsedRequest,
|
|
6084
|
+
fallbackPlan: configuredFallbackPlan,
|
|
6085
|
+
modelRouter,
|
|
6086
|
+
tracer,
|
|
6087
|
+
requestStartTime,
|
|
6088
|
+
logProxyBody,
|
|
6089
|
+
logFinalRequest,
|
|
6090
|
+
});
|
|
6091
|
+
if (configuredFallbackResult.response) {
|
|
6092
|
+
return configuredFallbackResult.response;
|
|
6093
|
+
}
|
|
6094
|
+
if (configuredFallbackResult.invalidRequestFailure) {
|
|
6095
|
+
return buildClaudeAnthropicFailureResponse({
|
|
6096
|
+
tracer,
|
|
6097
|
+
requestStartTime,
|
|
6098
|
+
authFailureMessage: null,
|
|
6099
|
+
authCooldownMessage: null,
|
|
6100
|
+
invalidRequestFailure: configuredFallbackResult.invalidRequestFailure,
|
|
6101
|
+
entitlementFailure: null,
|
|
6102
|
+
scopedExhaustion: null,
|
|
6103
|
+
sawNetworkError: false,
|
|
6104
|
+
sawTransientFailure: false,
|
|
6105
|
+
sawRateLimit: false,
|
|
6106
|
+
lastError: undefined,
|
|
6107
|
+
orderedAccounts: [],
|
|
6108
|
+
buildLoggedClaudeError,
|
|
6109
|
+
logProxyBody,
|
|
6110
|
+
logFinalRequest,
|
|
6111
|
+
});
|
|
6112
|
+
}
|
|
6113
|
+
return buildDeferredClaudeAccountFailureResponse({
|
|
6114
|
+
ctx,
|
|
6115
|
+
tracer,
|
|
6116
|
+
requestStartTime,
|
|
6117
|
+
failure: loadedAccounts.failure,
|
|
6118
|
+
buildLoggedClaudeError,
|
|
6119
|
+
});
|
|
5892
6120
|
}
|
|
5893
6121
|
const { accounts, enabledAccounts, orderedAccounts, bodyStr, requestStart, toolCount, url, clientHeaders, isClaudeClientRequest, } = loadedAccounts;
|
|
5894
6122
|
// Snapshot the operator policy once. Reading the module value later would let
|
|
@@ -6244,6 +6472,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
6244
6472
|
sawTransientFailure: loopState.sawTransientFailure,
|
|
6245
6473
|
invalidRequestFailure: loopState.invalidRequestFailure,
|
|
6246
6474
|
entitlementFailure: loopState.entitlementFailure,
|
|
6475
|
+
allowConfiguredModelFallback: hasConfiguredFallback,
|
|
6247
6476
|
});
|
|
6248
6477
|
loopState.lastError = nonOkResult.lastError;
|
|
6249
6478
|
loopState.authFailureMessage = nonOkResult.authFailureMessage;
|
|
@@ -6348,6 +6577,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
6348
6577
|
ctx,
|
|
6349
6578
|
body,
|
|
6350
6579
|
parsedFallbackRequest: parsedRequest,
|
|
6580
|
+
fallbackPlan: configuredFallbackPlan,
|
|
6351
6581
|
modelRouter,
|
|
6352
6582
|
tracer,
|
|
6353
6583
|
requestStartTime,
|
|
@@ -6357,10 +6587,20 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
6357
6587
|
if (configuredFallbackResult.response) {
|
|
6358
6588
|
return configuredFallbackResult.response;
|
|
6359
6589
|
}
|
|
6590
|
+
if (configuredFallbackResult.invalidRequestFailure &&
|
|
6591
|
+
!loopState.sawRateLimit) {
|
|
6592
|
+
// A converted Codex request can be rejected independently of the original
|
|
6593
|
+
// Anthropic request. Preserve a real pool 429 as the actionable terminal
|
|
6594
|
+
// response when both occurred.
|
|
6595
|
+
loopState.invalidRequestFailure =
|
|
6596
|
+
configuredFallbackResult.invalidRequestFailure;
|
|
6597
|
+
}
|
|
6360
6598
|
fallbackFailureMessage = configuredFallbackResult.lastErrorMessage;
|
|
6361
6599
|
// A translation-layer-selected provider is only permitted by an explicit
|
|
6362
6600
|
// routing setting. Empty fallback chains otherwise stay within OAuth.
|
|
6363
|
-
if (
|
|
6601
|
+
if (loopState.invalidRequestFailure === null &&
|
|
6602
|
+
!loopState.sawRateLimit &&
|
|
6603
|
+
modelRouter?.isAutoFallbackEnabled?.()) {
|
|
6364
6604
|
const autoFallbackResult = await tryAutoClaudeFallback({
|
|
6365
6605
|
ctx,
|
|
6366
6606
|
body,
|
|
@@ -7538,6 +7778,18 @@ export function isInvalidRequestError(status, errBody) {
|
|
|
7538
7778
|
return (parsed.errorType === "invalid_request_error" ||
|
|
7539
7779
|
errBody.includes("invalid_request_error"));
|
|
7540
7780
|
}
|
|
7781
|
+
/**
|
|
7782
|
+
* A 404 for a retired model can be served by an explicitly configured fallback;
|
|
7783
|
+
* other 404s remain terminal so a bad endpoint or resource is never disguised.
|
|
7784
|
+
*/
|
|
7785
|
+
function isAnthropicModelNotFound(status, errBody) {
|
|
7786
|
+
if (status !== 404) {
|
|
7787
|
+
return false;
|
|
7788
|
+
}
|
|
7789
|
+
const parsed = parseClaudeErrorBody(errBody);
|
|
7790
|
+
return (parsed.errorType === "not_found_error" &&
|
|
7791
|
+
(parsed.message ?? "").toLowerCase().includes("model"));
|
|
7792
|
+
}
|
|
7541
7793
|
/**
|
|
7542
7794
|
* A subscription-specific beta rejection. Anthropic returns
|
|
7543
7795
|
* `400 invalid_request_error` with a message like
|
|
@@ -7764,7 +8016,10 @@ export const __testHooks = {
|
|
|
7764
8016
|
redactProviderErrorMessage,
|
|
7765
8017
|
isUpstreamOverload,
|
|
7766
8018
|
getOverloadRotationDelayMs,
|
|
8019
|
+
redactHeadersForBorrower,
|
|
7767
8020
|
shouldAttemptClaudeFallback,
|
|
8021
|
+
isAnthropicModelNotFound,
|
|
8022
|
+
getCodexFallbackInvalidRequestFailure,
|
|
7768
8023
|
executeClaudeFallbackWithRetry,
|
|
7769
8024
|
buildClaudeAnthropicFailureResponse,
|
|
7770
8025
|
isAccountEntitlementError,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* never collides with anthropic entries) and does pre-commit rotation only, not
|
|
18
18
|
* the full transient-budget / admission machinery.
|
|
19
19
|
*/
|
|
20
|
-
import type { AccountCoolingReason, AccountQuota, CodexRuntimeAccount, RouteGroup } from "../../types/index.js";
|
|
20
|
+
import type { AccountCoolingReason, AccountQuota, CodexRuntimeAccount, RouteGroup, ServerContext } from "../../types/index.js";
|
|
21
21
|
/** Refresh an account's token at most once at a time. */
|
|
22
22
|
declare function refreshCodexTokenOnce(key: string, refreshToken: string): Promise<{
|
|
23
23
|
accessToken: string;
|
|
@@ -46,6 +46,8 @@ declare function planCodexCooldown(quota: AccountQuota | null, retryAfterMs: num
|
|
|
46
46
|
coolingUntil: number;
|
|
47
47
|
reason: AccountCoolingReason;
|
|
48
48
|
};
|
|
49
|
+
/** Core pooled handler for POST /backend-api/codex/responses. */
|
|
50
|
+
export declare function handleCodexResponsesRequest(ctx: ServerContext): Promise<Response>;
|
|
49
51
|
/**
|
|
50
52
|
* Create Codex proxy routes.
|
|
51
53
|
*
|