@juspay/neurolink 12.17.1 → 12.17.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 +6 -2
- package/dist/browser/neurolink.min.js +430 -421
- package/dist/cli/commands/proxy.js +455 -259
- package/dist/constants/contextWindows.d.ts +2 -0
- package/dist/constants/contextWindows.js +4 -0
- package/dist/neurolink.js +8 -3
- package/dist/providers/openaiChatCompletionsBase.js +60 -51
- package/dist/proxy/anthropicBudgetResponse.d.ts +3 -0
- package/dist/proxy/anthropicBudgetResponse.js +153 -0
- package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
- package/dist/proxy/bodyCaptureProcessing.js +23 -4
- package/dist/proxy/bodyCaptureWorker.js +20 -2
- package/dist/proxy/codexFallback.d.ts +10 -0
- package/dist/proxy/codexFallback.js +108 -24
- package/dist/proxy/codexUsage.js +2 -0
- package/dist/proxy/globalInstaller.d.ts +32 -1
- package/dist/proxy/globalInstaller.js +386 -3
- package/dist/proxy/otelLogSink.d.ts +3 -3
- package/dist/proxy/otelLogSink.js +137 -144
- package/dist/proxy/otlpLogTransport.d.ts +3 -0
- package/dist/proxy/otlpLogTransport.js +334 -0
- package/dist/proxy/proxyActivity.d.ts +22 -2
- package/dist/proxy/proxyActivity.js +114 -9
- package/dist/proxy/proxyAnalysis.js +82 -52
- package/dist/proxy/proxyClientCancellation.d.ts +7 -0
- package/dist/proxy/proxyClientCancellation.js +23 -0
- package/dist/proxy/proxyContextPreflight.d.ts +25 -0
- package/dist/proxy/proxyContextPreflight.js +337 -0
- package/dist/proxy/proxyFailureDetails.d.ts +19 -0
- package/dist/proxy/proxyFailureDetails.js +96 -0
- package/dist/proxy/proxyLifecycle.js +7 -0
- package/dist/proxy/proxyRouteBodyCapture.d.ts +13 -0
- package/dist/proxy/proxyRouteBodyCapture.js +112 -0
- package/dist/proxy/proxyTokenBudget.d.ts +40 -0
- package/dist/proxy/proxyTokenBudget.js +523 -0
- package/dist/proxy/proxyTracer.d.ts +3 -2
- package/dist/proxy/proxyTracer.js +46 -20
- package/dist/proxy/proxyTranslationEngine.d.ts +8 -10
- package/dist/proxy/proxyTranslationEngine.js +736 -376
- package/dist/proxy/requestLogger.d.ts +1 -0
- package/dist/proxy/requestLogger.js +107 -5
- package/dist/proxy/restartControl.js +9 -4
- package/dist/proxy/rollingWorkerProcess.js +65 -0
- package/dist/proxy/rollingWorkerSupervisor.js +7 -0
- package/dist/proxy/updateCoordinator.d.ts +14 -1
- package/dist/proxy/updateCoordinator.js +57 -2
- package/dist/server/routes/claudeProxyRoutes.d.ts +4 -5
- package/dist/server/routes/claudeProxyRoutes.js +809 -577
- package/dist/server/routes/codexProxyRoutes.js +191 -9
- package/dist/server/routes/openaiProxyRoutes.d.ts +1 -1
- package/dist/server/routes/openaiProxyRoutes.js +261 -69
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/codex.d.ts +5 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/proxy.d.ts +148 -3
- package/dist/types/proxy.js +0 -15
- package/dist/types/proxyBudget.d.ts +74 -0
- package/dist/types/proxyBudget.js +1 -0
- package/dist/types/proxyContext.d.ts +35 -0
- package/dist/types/proxyContext.js +1 -0
- package/dist/utils/fileDetector.js +28 -7
- package/dist/utils/messageBuilder.js +51 -26
- package/dist/utils/pdfProcessor.js +26 -4
- package/docs/assets/dashboards/README.md +37 -0
- package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +197 -89
- package/docs-site/static/search-index.json +1 -1
- package/package.json +7 -2
- package/scripts/observability/NATIVE-DURABLE.md +63 -0
- package/scripts/observability/otel-collector.proxy-native-durable.yaml +93 -0
- package/scripts/observability/proxy-telemetry-backend.mjs +32 -5
- package/scripts/observability/proxy-telemetry-check.mjs +396 -61
- package/scripts/observability/query-proxy-history.mjs +34 -5
|
@@ -25,10 +25,11 @@ import { AccountQuotaRefreshCoordinator } from "../../proxy/accountQuotaRefreshC
|
|
|
25
25
|
import { ProviderTransportCoordinator } from "../../proxy/providerTransportCoordinator.js";
|
|
26
26
|
import { MAX_COOLDOWN_MS_BY_REASON } from "../../proxy/routingEvidence.js";
|
|
27
27
|
import { buildProxyLimitHeaders, summarizePoolHeadroom, } from "../../proxy/quotaHeaders.js";
|
|
28
|
-
import { buildClaudeError, ClaudeStreamSerializer,
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
28
|
+
import { buildClaudeError, ClaudeStreamSerializer, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
|
|
29
|
+
import { getProxyUpstreamFailure } from "../../proxy/proxyFailureDetails.js";
|
|
30
|
+
import { CodexFallbackResponseError, CodexFallbackStreamError, consumeCodexFallbackResponse, createCodexFallbackStream, convertClaudeRequestToCodex, } from "../../proxy/codexFallback.js";
|
|
31
|
+
import { isProxyRequestFinalized, getProxyRequestAccounting, registerProxyResponseObserver, takeProxyResponseObservers, trackProxyResponse, } from "../../proxy/proxyActivity.js";
|
|
32
|
+
import { buildAnthropicModelsListResponse, buildTranslationOptions, handleTranslatedJsonRequest, handleTranslatedStreamRequest, } from "../../proxy/proxyTranslationEngine.js";
|
|
32
33
|
import { tracers } from "../../telemetry/tracers.js";
|
|
33
34
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
34
35
|
import { ProxyTracer, recordFallbackAttempt } from "../../proxy/proxyTracer.js";
|
|
@@ -152,8 +153,43 @@ const AUTH_REFRESH_MAX_COOLDOWN_MS = 5 * 60 * 1000;
|
|
|
152
153
|
* to cover the full lifecycle of streaming responses, including extended
|
|
153
154
|
* thinking from Opus models (which can exceed 5 minutes for large contexts). */
|
|
154
155
|
const UPSTREAM_FETCH_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
|
|
156
|
+
// Context checks and spending reservations belong immediately before dispatch.
|
|
157
|
+
import { prepareProxyRequestContext, ProxyContextPreflightError, } from "../../proxy/proxyContextPreflight.js";
|
|
158
|
+
import { reserveProxyTokenBudget, getProxyTokenBudgetError, getProxyTokenBudgetSessionKey, } from "../../proxy/proxyTokenBudget.js";
|
|
159
|
+
import { observeAnthropicBudgetResponse } from "../../proxy/anthropicBudgetResponse.js";
|
|
160
|
+
function prepareAnthropicWire(ctx, bodyStr) {
|
|
161
|
+
const body = JSON.parse(bodyStr);
|
|
162
|
+
const prepared = prepareProxyRequestContext({
|
|
163
|
+
provider: "anthropic",
|
|
164
|
+
model: body.model,
|
|
165
|
+
body,
|
|
166
|
+
});
|
|
167
|
+
ctx.metadata.contextPreflight = prepared.evidence;
|
|
168
|
+
return {
|
|
169
|
+
bodyStr: prepared.body === body ? bodyStr : JSON.stringify(prepared.body),
|
|
170
|
+
preparedContext: prepared,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
155
173
|
let anthropicUpstreamDispatcher;
|
|
156
|
-
function fetchAnthropicUpstream(url, init) {
|
|
174
|
+
async function fetchAnthropicUpstream(url, init, ctx, accountKey, dispatch) {
|
|
175
|
+
clearClaudeFallbackTerminalEvidence(ctx);
|
|
176
|
+
const prepared = dispatch.preparedContext;
|
|
177
|
+
const lease = await reserveProxyTokenBudget({
|
|
178
|
+
provider: "anthropic",
|
|
179
|
+
accountKey,
|
|
180
|
+
requestId: ctx.requestId,
|
|
181
|
+
sessionKey: getProxyTokenBudgetSessionKey({
|
|
182
|
+
get: (name) => ctx.headers[name] ?? null,
|
|
183
|
+
}),
|
|
184
|
+
reservationTokens: prepared.totalTokensReservation,
|
|
185
|
+
estimateProvenance: "estimated_input_plus_output_reserve",
|
|
186
|
+
});
|
|
187
|
+
ctx.metadata.tokenBudget = lease.snapshot;
|
|
188
|
+
if (init.signal?.aborted || ctx.abortSignal?.aborted) {
|
|
189
|
+
await lease.cancelBeforeDispatch();
|
|
190
|
+
ctx.metadata.tokenBudget = lease.snapshot;
|
|
191
|
+
throw init.signal?.reason ?? ctx.abortSignal?.reason;
|
|
192
|
+
}
|
|
157
193
|
// Node's global fetch applies Undici's 300s default headers timeout before
|
|
158
194
|
// the route's 15-minute abort signal. Keep both transport deadlines aligned
|
|
159
195
|
// with the proxy contract and instantiate lazily so importing routes has no
|
|
@@ -162,10 +198,36 @@ function fetchAnthropicUpstream(url, init) {
|
|
|
162
198
|
headersTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
|
|
163
199
|
bodyTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
|
|
164
200
|
});
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
201
|
+
try {
|
|
202
|
+
const dispatchedBody = String(init.body);
|
|
203
|
+
recordAttempt(dispatch.accountLabel, dispatch.accountType);
|
|
204
|
+
dispatch.tracer?.logUpstreamRequestHeaders(init.headers);
|
|
205
|
+
dispatch.tracer?.logUpstreamRequestBody(dispatchedBody);
|
|
206
|
+
dispatch.logProxyBody({
|
|
207
|
+
phase: "upstream_request",
|
|
208
|
+
headers: init.headers,
|
|
209
|
+
body: dispatchedBody,
|
|
210
|
+
bodySize: Buffer.byteLength(dispatchedBody, "utf8"),
|
|
211
|
+
contentType: "application/json",
|
|
212
|
+
account: dispatch.accountLabel,
|
|
213
|
+
accountType: dispatch.accountType,
|
|
214
|
+
attempt: dispatch.attemptNumber,
|
|
215
|
+
metadata: { upstreamMethod: "POST", upstreamUrl: url },
|
|
216
|
+
});
|
|
217
|
+
const response = await fetch(url, {
|
|
218
|
+
...init,
|
|
219
|
+
body: dispatchedBody,
|
|
220
|
+
dispatcher: anthropicUpstreamDispatcher,
|
|
221
|
+
});
|
|
222
|
+
return observeAnthropicBudgetResponse(response, lease, () => {
|
|
223
|
+
ctx.metadata.tokenBudget = lease.snapshot;
|
|
224
|
+
}, init.signal ?? undefined);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
await lease.settle().catch(() => undefined);
|
|
228
|
+
ctx.metadata.tokenBudget = lease.snapshot;
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
169
231
|
}
|
|
170
232
|
const accountRuntimeState = new Map();
|
|
171
233
|
const accountQuotaRefreshCoordinator = new AccountQuotaRefreshCoordinator();
|
|
@@ -2204,7 +2266,7 @@ function logProxyRoutingPlan(logProxyBody, stage, plan) {
|
|
|
2204
2266
|
async function handleClaudePassthroughRequest(args) {
|
|
2205
2267
|
const { ctx, body, clientRequestBody, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
|
|
2206
2268
|
tracer?.setMode("passthrough-cli");
|
|
2207
|
-
const bodyStr = clientRequestBody;
|
|
2269
|
+
const { bodyStr, preparedContext } = prepareAnthropicWire(ctx, clientRequestBody);
|
|
2208
2270
|
const toolCount = Array.isArray(body.tools) ? body.tools.length : 0;
|
|
2209
2271
|
const upstreamHeaders = {};
|
|
2210
2272
|
for (const [key, value] of Object.entries(ctx.headers)) {
|
|
@@ -2222,33 +2284,32 @@ async function handleClaudePassthroughRequest(args) {
|
|
|
2222
2284
|
polyfillBody: false,
|
|
2223
2285
|
upstreamUrl: "https://api.anthropic.com/v1/messages?beta=true",
|
|
2224
2286
|
});
|
|
2225
|
-
tracer?.logUpstreamRequestHeaders(upstreamHeaders);
|
|
2226
|
-
tracer?.logUpstreamRequestBody(bodyStr);
|
|
2227
|
-
logProxyBody({
|
|
2228
|
-
phase: "upstream_request",
|
|
2229
|
-
headers: upstreamHeaders,
|
|
2230
|
-
body: bodyStr,
|
|
2231
|
-
bodySize: Buffer.byteLength(bodyStr, "utf8"),
|
|
2232
|
-
contentType: upstreamHeaders["content-type"] ?? "application/json",
|
|
2233
|
-
account: "passthrough",
|
|
2234
|
-
accountType: "passthrough",
|
|
2235
|
-
attempt: 1,
|
|
2236
|
-
metadata: {
|
|
2237
|
-
upstreamMethod: "POST",
|
|
2238
|
-
upstreamUrl: "https://api.anthropic.com/v1/messages?beta=true",
|
|
2239
|
-
},
|
|
2240
|
-
});
|
|
2241
|
-
recordAttempt("passthrough", "passthrough");
|
|
2242
2287
|
let response;
|
|
2243
2288
|
try {
|
|
2244
2289
|
response = await fetchAnthropicUpstream("https://api.anthropic.com/v1/messages?beta=true", {
|
|
2245
2290
|
method: "POST",
|
|
2246
2291
|
headers: upstreamHeaders,
|
|
2247
2292
|
body: bodyStr,
|
|
2248
|
-
signal: AbortSignal.
|
|
2293
|
+
signal: AbortSignal.any([
|
|
2294
|
+
...(ctx.abortSignal ? [ctx.abortSignal] : []),
|
|
2295
|
+
AbortSignal.timeout(UPSTREAM_FETCH_TIMEOUT_MS),
|
|
2296
|
+
]),
|
|
2297
|
+
}, ctx, "anthropic:passthrough", {
|
|
2298
|
+
logProxyBody,
|
|
2299
|
+
tracer,
|
|
2300
|
+
attemptNumber: 1,
|
|
2301
|
+
accountLabel: "passthrough",
|
|
2302
|
+
accountType: "passthrough",
|
|
2303
|
+
preparedContext,
|
|
2249
2304
|
});
|
|
2250
2305
|
}
|
|
2251
2306
|
catch (fetchErr) {
|
|
2307
|
+
if (fetchErr instanceof ProxyContextPreflightError ||
|
|
2308
|
+
getProxyTokenBudgetError(fetchErr) ||
|
|
2309
|
+
ctx.abortSignal?.aborted) {
|
|
2310
|
+
upstreamSpan?.end();
|
|
2311
|
+
throw fetchErr;
|
|
2312
|
+
}
|
|
2252
2313
|
const errMsg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
2253
2314
|
recordAttemptError("passthrough", "passthrough", 502);
|
|
2254
2315
|
tracer?.setError("network_error", errMsg);
|
|
@@ -3505,134 +3566,47 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
3505
3566
|
isClaudeClientRequest: isLikelyClaudeClient(clientHeaders, clientSnapshotBody),
|
|
3506
3567
|
};
|
|
3507
3568
|
}
|
|
3508
|
-
|
|
3509
|
-
const
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
catch (error) {
|
|
3519
|
-
fallbackAbortController.abort(error);
|
|
3520
|
-
throw error;
|
|
3569
|
+
function clearClaudeFallbackTerminalEvidence(ctx) {
|
|
3570
|
+
for (const key of [
|
|
3571
|
+
"codexFallbackAccount",
|
|
3572
|
+
"codexFallbackAccountType",
|
|
3573
|
+
"codexFallbackModel",
|
|
3574
|
+
"codexFallbackFailureUsage",
|
|
3575
|
+
"codexFallbackDeferredFailure",
|
|
3576
|
+
"sdkFallbackFailure",
|
|
3577
|
+
]) {
|
|
3578
|
+
delete ctx.metadata[key];
|
|
3521
3579
|
}
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
fallbackAbortController.abort(error);
|
|
3540
|
-
void iterator.return?.().catch(() => {
|
|
3541
|
-
// Timeout/error already determines the request outcome.
|
|
3542
|
-
});
|
|
3543
|
-
throw error;
|
|
3544
|
-
}
|
|
3580
|
+
}
|
|
3581
|
+
async function executeClaudeFallbackTranslation(args) {
|
|
3582
|
+
const { ctx, body, tracer, requestStartTime, options, providerLabel, idleTimeoutMs = FALLBACK_STREAM_IDLE_TIMEOUT_MS, } = args;
|
|
3583
|
+
clearClaudeFallbackTerminalEvidence(ctx);
|
|
3584
|
+
const parsed = parseClaudeRequest(body);
|
|
3585
|
+
const provider = typeof options.provider === "string" ? options.provider : undefined;
|
|
3586
|
+
const model = typeof options.model === "string" ? options.model : undefined;
|
|
3587
|
+
const shared = {
|
|
3588
|
+
ctx,
|
|
3589
|
+
format: "claude",
|
|
3590
|
+
requestModel: body.model,
|
|
3591
|
+
parsed,
|
|
3592
|
+
attempts: [{ provider, model, label: providerLabel }],
|
|
3593
|
+
tracer,
|
|
3594
|
+
requestStartTime,
|
|
3595
|
+
options,
|
|
3596
|
+
attemptTimeoutMs: idleTimeoutMs,
|
|
3545
3597
|
};
|
|
3546
3598
|
if (body.stream) {
|
|
3547
|
-
|
|
3548
|
-
//
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
frames.push(frame);
|
|
3553
|
-
}
|
|
3554
|
-
const collectedText = await consumeFallbackStream((text) => {
|
|
3555
|
-
for (const frame of serializer.pushDelta(text)) {
|
|
3556
|
-
frames.push(frame);
|
|
3557
|
-
}
|
|
3558
|
-
});
|
|
3559
|
-
const toolCalls = streamResult.toolCalls ?? [];
|
|
3560
|
-
if (!hasTranslatedOutput(collectedText, toolCalls)) {
|
|
3561
|
-
throw new Error(`Translated provider ${providerLabel} returned no content or tool calls`);
|
|
3562
|
-
}
|
|
3563
|
-
if (toolCalls.length) {
|
|
3564
|
-
for (const toolCall of toolCalls) {
|
|
3565
|
-
const toolName = toolCall.toolName ??
|
|
3566
|
-
toolCall.name ??
|
|
3567
|
-
"unknown";
|
|
3568
|
-
for (const frame of serializer.pushToolUse(generateToolUseId(), toolName, extractToolArgs(toolCall))) {
|
|
3569
|
-
frames.push(frame);
|
|
3570
|
-
}
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
const reason = streamResult.finishReason ?? "end_turn";
|
|
3574
|
-
const resolvedUsage = extractUsageFromStreamResult(streamResult.usage);
|
|
3575
|
-
for (const frame of serializer.finish(resolvedUsage.output, reason)) {
|
|
3576
|
-
frames.push(frame);
|
|
3577
|
-
}
|
|
3578
|
-
ctx.metadata.firstUsefulOutputAt = Date.now();
|
|
3579
|
-
ctx.metadata.firstUsefulOutputEvent = "translated_response.ready";
|
|
3580
|
-
// Telemetry AFTER validation — not before like the old lazy path
|
|
3581
|
-
tracer?.end(200, Date.now() - requestStartTime);
|
|
3582
|
-
logFinalRequest(200, "", providerLabel, undefined, undefined, {
|
|
3583
|
-
inputTokens: resolvedUsage.input,
|
|
3584
|
-
outputTokens: resolvedUsage.output,
|
|
3585
|
-
});
|
|
3586
|
-
const bufferedBody = frames.join("");
|
|
3587
|
-
logProxyBody({
|
|
3588
|
-
phase: "client_response",
|
|
3589
|
-
headers: { "content-type": "text/event-stream" },
|
|
3590
|
-
body: bufferedBody,
|
|
3591
|
-
bodySize: Buffer.byteLength(bufferedBody, "utf8"),
|
|
3592
|
-
contentType: "text/event-stream",
|
|
3593
|
-
responseStatus: 200,
|
|
3594
|
-
durationMs: Date.now() - requestStartTime,
|
|
3599
|
+
// Prime only the first actual upstream output. Errors before it still reach
|
|
3600
|
+
// the configured fallback chain; after return no delivered output is replayed.
|
|
3601
|
+
return handleTranslatedStreamRequest({
|
|
3602
|
+
...shared,
|
|
3603
|
+
prefetchFirstOutput: true,
|
|
3595
3604
|
});
|
|
3596
|
-
// Return generator that yields pre-buffered frames
|
|
3597
|
-
async function* sseGenerator() {
|
|
3598
|
-
for (const frame of frames) {
|
|
3599
|
-
yield frame;
|
|
3600
|
-
}
|
|
3601
|
-
}
|
|
3602
|
-
return sseGenerator();
|
|
3603
|
-
}
|
|
3604
|
-
const collectedText = await consumeFallbackStream();
|
|
3605
|
-
if (!hasTranslatedOutput(collectedText, streamResult.toolCalls)) {
|
|
3606
|
-
throw new Error(`Translated provider ${providerLabel} returned no content or tool calls`);
|
|
3607
3605
|
}
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
finishReason: streamResult.finishReason ?? "end_turn",
|
|
3612
|
-
reasoning: undefined,
|
|
3613
|
-
usage: streamResult.usage
|
|
3614
|
-
? extractUsageFromStreamResult(streamResult.usage)
|
|
3615
|
-
: undefined,
|
|
3616
|
-
toolCalls: streamResult.toolCalls,
|
|
3617
|
-
};
|
|
3618
|
-
tracer?.end(200, Date.now() - requestStartTime);
|
|
3619
|
-
const clientResponse = serializeClaudeResponse(internal, body.model);
|
|
3620
|
-
observeClaudeJsonOutput(ctx.metadata, clientResponse);
|
|
3621
|
-
logFinalRequest(200, "", providerLabel, undefined, undefined, {
|
|
3622
|
-
inputTokens: internal.usage?.input,
|
|
3623
|
-
outputTokens: internal.usage?.output,
|
|
3624
|
-
});
|
|
3625
|
-
const clientResponseText = JSON.stringify(clientResponse);
|
|
3626
|
-
logProxyBody({
|
|
3627
|
-
phase: "client_response",
|
|
3628
|
-
headers: { "content-type": "application/json" },
|
|
3629
|
-
body: clientResponseText,
|
|
3630
|
-
bodySize: Buffer.byteLength(clientResponseText, "utf8"),
|
|
3631
|
-
contentType: "application/json",
|
|
3632
|
-
responseStatus: 200,
|
|
3633
|
-
durationMs: Date.now() - requestStartTime,
|
|
3606
|
+
return handleTranslatedJsonRequest({
|
|
3607
|
+
...shared,
|
|
3608
|
+
deferFinalOnError: true,
|
|
3634
3609
|
});
|
|
3635
|
-
return clientResponse;
|
|
3636
3610
|
}
|
|
3637
3611
|
async function executeClaudeFallbackWithRetry(args) {
|
|
3638
3612
|
let lastError;
|
|
@@ -3642,7 +3616,8 @@ async function executeClaudeFallbackWithRetry(args) {
|
|
|
3642
3616
|
}
|
|
3643
3617
|
catch (error) {
|
|
3644
3618
|
lastError = error;
|
|
3645
|
-
if (
|
|
3619
|
+
if (getProxyUpstreamFailure(error)?.retryable === false ||
|
|
3620
|
+
!isRetryableNetworkError(error) ||
|
|
3646
3621
|
retry === MAX_FALLBACK_NETWORK_RETRIES) {
|
|
3647
3622
|
throw error;
|
|
3648
3623
|
}
|
|
@@ -3661,12 +3636,14 @@ async function executeClaudeFallbackWithRetry(args) {
|
|
|
3661
3636
|
*/
|
|
3662
3637
|
async function executeClaudeCodexFallback(args) {
|
|
3663
3638
|
const { ctx, body, model, reasoningEffort, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
|
|
3639
|
+
clearClaudeFallbackTerminalEvidence(ctx);
|
|
3664
3640
|
const codexCtx = {
|
|
3665
3641
|
...ctx,
|
|
3666
3642
|
requestId: `${ctx.requestId}:codex-fallback`,
|
|
3667
3643
|
method: "POST",
|
|
3668
3644
|
path: "/backend-api/codex/responses",
|
|
3669
3645
|
headers: {
|
|
3646
|
+
...ctx.headers,
|
|
3670
3647
|
"content-type": "application/json",
|
|
3671
3648
|
accept: "text/event-stream",
|
|
3672
3649
|
},
|
|
@@ -3693,19 +3670,23 @@ async function executeClaudeCodexFallback(args) {
|
|
|
3693
3670
|
registerProxyResponseObserver(ctx.metadata, {
|
|
3694
3671
|
onTerminal: () => childAccounting,
|
|
3695
3672
|
});
|
|
3696
|
-
const recordCodexUsage = (result) => {
|
|
3673
|
+
const recordCodexUsage = (result, terminal = true) => {
|
|
3697
3674
|
ctx.metadata.codexFallbackModel = model;
|
|
3675
|
+
if (!terminal) {
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3698
3678
|
try {
|
|
3699
3679
|
tracer?.setModelSubstitution(body.model, model, "openai");
|
|
3700
3680
|
tracer?.setServedAccount(codexHeaders["x-neurolink-account"] ?? "unknown", "codex-oauth");
|
|
3701
|
-
if (result?.usage
|
|
3681
|
+
if (result?.usage?.inputTokensObserved &&
|
|
3682
|
+
result.usage.outputTokensObserved) {
|
|
3702
3683
|
tracer?.setUsage({
|
|
3703
3684
|
inputTokens: result.usage.input,
|
|
3704
3685
|
outputTokens: result.usage.output,
|
|
3705
3686
|
cacheReadTokens: result.usage.cacheReadTokens ?? 0,
|
|
3706
3687
|
cacheCreationTokens: result.usage.cacheCreationTokens ?? 0,
|
|
3707
3688
|
reasoningTokens: result.usage.reasoning,
|
|
3708
|
-
});
|
|
3689
|
+
}, { recordMetrics: false });
|
|
3709
3690
|
}
|
|
3710
3691
|
}
|
|
3711
3692
|
catch {
|
|
@@ -3713,6 +3694,9 @@ async function executeClaudeCodexFallback(args) {
|
|
|
3713
3694
|
}
|
|
3714
3695
|
};
|
|
3715
3696
|
const codexHeaders = { ...(codexCtx.responseHeaders ?? {}) };
|
|
3697
|
+
ctx.metadata.codexFallbackAccount = codexHeaders["x-neurolink-account"];
|
|
3698
|
+
ctx.metadata.codexFallbackAccountType =
|
|
3699
|
+
codexHeaders["x-neurolink-account-type"] ?? "codex-oauth";
|
|
3716
3700
|
if (body.stream) {
|
|
3717
3701
|
const bridge = await createCodexFallbackStream(codexResponse, body.model);
|
|
3718
3702
|
ctx.responseHeaders ??= {};
|
|
@@ -3722,7 +3706,8 @@ async function executeClaudeCodexFallback(args) {
|
|
|
3722
3706
|
let settled = false;
|
|
3723
3707
|
let captured = "";
|
|
3724
3708
|
let responseBytes = 0;
|
|
3725
|
-
|
|
3709
|
+
let captureTruncated = false;
|
|
3710
|
+
const finish = (status, result, errorType, message, failure) => {
|
|
3726
3711
|
if (settled) {
|
|
3727
3712
|
return;
|
|
3728
3713
|
}
|
|
@@ -3734,11 +3719,17 @@ async function executeClaudeCodexFallback(args) {
|
|
|
3734
3719
|
recordCodexUsage(result);
|
|
3735
3720
|
tracer?.end(status, Date.now() - requestStartTime);
|
|
3736
3721
|
logFinalRequest(status, account, accountType, errorType, message, {
|
|
3737
|
-
inputTokens: result?.usage?.
|
|
3738
|
-
|
|
3722
|
+
inputTokens: result?.usage?.inputTokensObserved
|
|
3723
|
+
? result.usage.input
|
|
3724
|
+
: undefined,
|
|
3725
|
+
outputTokens: result?.usage?.outputTokensObserved
|
|
3726
|
+
? result.usage.output
|
|
3727
|
+
: undefined,
|
|
3739
3728
|
cacheCreationTokens: result?.usage?.cacheCreationTokens,
|
|
3740
3729
|
cacheReadTokens: result?.usage?.cacheReadTokens,
|
|
3741
3730
|
reasoningTokens: result?.usage?.reasoning,
|
|
3731
|
+
errorCode: failure?.code,
|
|
3732
|
+
retryable: failure?.retryable,
|
|
3742
3733
|
});
|
|
3743
3734
|
recordFallbackAttempt({
|
|
3744
3735
|
provider: "codex",
|
|
@@ -3752,6 +3743,7 @@ async function executeClaudeCodexFallback(args) {
|
|
|
3752
3743
|
contentType: "text/event-stream",
|
|
3753
3744
|
body: captured,
|
|
3754
3745
|
bodySize: responseBytes,
|
|
3746
|
+
sourceTruncated: captureTruncated,
|
|
3755
3747
|
responseStatus: status,
|
|
3756
3748
|
durationMs: Date.now() - requestStartTime,
|
|
3757
3749
|
});
|
|
@@ -3774,6 +3766,9 @@ async function executeClaudeCodexFallback(args) {
|
|
|
3774
3766
|
});
|
|
3775
3767
|
const capture = (frame) => {
|
|
3776
3768
|
responseBytes += Buffer.byteLength(frame);
|
|
3769
|
+
if (captured.length + frame.length > 1024 * 1024) {
|
|
3770
|
+
captureTruncated = true;
|
|
3771
|
+
}
|
|
3777
3772
|
if (captured.length < 1024 * 1024) {
|
|
3778
3773
|
captured += frame.slice(0, 1024 * 1024 - captured.length);
|
|
3779
3774
|
}
|
|
@@ -3837,11 +3832,24 @@ async function executeClaudeCodexFallback(args) {
|
|
|
3837
3832
|
if (!settled) {
|
|
3838
3833
|
const detail = redactProviderErrorMessage(describeTransportError(error));
|
|
3839
3834
|
logger.always(`[proxy] Codex fallback stream failed: ${detail}`);
|
|
3840
|
-
const
|
|
3835
|
+
const failure = error instanceof CodexFallbackStreamError ? error : undefined;
|
|
3836
|
+
const status = failure?.status ?? 502;
|
|
3837
|
+
const errorBody = buildClaudeError(status, detail);
|
|
3838
|
+
Object.assign(errorBody.error, {
|
|
3839
|
+
code: failure?.code,
|
|
3840
|
+
retryable: failure?.retryable,
|
|
3841
|
+
});
|
|
3841
3842
|
const frames = [
|
|
3842
|
-
|
|
3843
|
-
]
|
|
3844
|
-
finish(
|
|
3843
|
+
capture(`event: error\ndata: ${JSON.stringify(errorBody)}\n\n`),
|
|
3844
|
+
];
|
|
3845
|
+
finish(status, failure?.usage
|
|
3846
|
+
? {
|
|
3847
|
+
text: "",
|
|
3848
|
+
toolCalls: [],
|
|
3849
|
+
finishReason: "end_turn",
|
|
3850
|
+
usage: failure.usage,
|
|
3851
|
+
}
|
|
3852
|
+
: undefined, "stream_error", detail, failure);
|
|
3845
3853
|
yield* frames;
|
|
3846
3854
|
}
|
|
3847
3855
|
}
|
|
@@ -3863,9 +3871,22 @@ async function executeClaudeCodexFallback(args) {
|
|
|
3863
3871
|
parsed = await consumeCodexFallbackResponse(codexResponse);
|
|
3864
3872
|
}
|
|
3865
3873
|
catch (error) {
|
|
3874
|
+
ctx.metadata.codexFallbackModel = model;
|
|
3875
|
+
ctx.metadata.codexFallbackDeferredFailure = true;
|
|
3866
3876
|
if (error instanceof CodexFallbackResponseError) {
|
|
3867
3877
|
logger.always(`[proxy] Codex fallback returned ${error.status}: ${sanitizeForLog(error.responseBody, 500)}`);
|
|
3868
3878
|
}
|
|
3879
|
+
if (error instanceof CodexFallbackStreamError) {
|
|
3880
|
+
recordCodexUsage(error.usage
|
|
3881
|
+
? {
|
|
3882
|
+
text: "",
|
|
3883
|
+
toolCalls: [],
|
|
3884
|
+
finishReason: "end_turn",
|
|
3885
|
+
usage: error.usage,
|
|
3886
|
+
}
|
|
3887
|
+
: undefined, false);
|
|
3888
|
+
ctx.metadata.codexFallbackFailureUsage = error.usage;
|
|
3889
|
+
}
|
|
3869
3890
|
throw error;
|
|
3870
3891
|
}
|
|
3871
3892
|
if (Object.keys(codexHeaders).length > 0) {
|
|
@@ -3888,8 +3909,12 @@ async function executeClaudeCodexFallback(args) {
|
|
|
3888
3909
|
const clientResponse = serializeClaudeResponse(internal, body.model);
|
|
3889
3910
|
observeClaudeJsonOutput(ctx.metadata, clientResponse);
|
|
3890
3911
|
logFinalRequest(200, accountLabel, accountType, undefined, undefined, {
|
|
3891
|
-
inputTokens: parsed.usage?.
|
|
3892
|
-
|
|
3912
|
+
inputTokens: parsed.usage?.inputTokensObserved
|
|
3913
|
+
? parsed.usage.input
|
|
3914
|
+
: undefined,
|
|
3915
|
+
outputTokens: parsed.usage?.outputTokensObserved
|
|
3916
|
+
? parsed.usage.output
|
|
3917
|
+
: undefined,
|
|
3893
3918
|
cacheCreationTokens: parsed.usage?.cacheCreationTokens,
|
|
3894
3919
|
cacheReadTokens: parsed.usage?.cacheReadTokens,
|
|
3895
3920
|
reasoningTokens: parsed.usage?.reasoning,
|
|
@@ -4080,25 +4105,33 @@ async function tryConfiguredClaudeFallbackChain(args) {
|
|
|
4080
4105
|
return { response };
|
|
4081
4106
|
}
|
|
4082
4107
|
catch (fallbackErr) {
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4108
|
+
if (fallbackErr instanceof ProxyContextPreflightError ||
|
|
4109
|
+
getProxyTokenBudgetError(fallbackErr)) {
|
|
4110
|
+
throw fallbackErr;
|
|
4111
|
+
}
|
|
4112
|
+
const failure = getProxyUpstreamFailure(fallbackErr);
|
|
4113
|
+
const status = ctx.abortSignal?.aborted ? 499 : (failure?.status ?? 502);
|
|
4088
4114
|
terminalFailure = {
|
|
4089
4115
|
status,
|
|
4090
|
-
message:
|
|
4116
|
+
message: failure?.message
|
|
4117
|
+
? sanitizeForLog(`Configured fallback ${fallback.provider}/${fallback.model} failed: ${failure.message}`, 500)
|
|
4118
|
+
: `Configured fallback ${fallback.provider}/${fallback.model} failed (HTTP ${status})`,
|
|
4119
|
+
...(failure
|
|
4120
|
+
? { errorCode: failure.code, retryable: failure.retryable }
|
|
4121
|
+
: {}),
|
|
4091
4122
|
errorType: status === 499
|
|
4092
4123
|
? "client_cancelled"
|
|
4093
|
-
: status ===
|
|
4094
|
-
? "
|
|
4095
|
-
: status ===
|
|
4096
|
-
? "
|
|
4097
|
-
: status ===
|
|
4098
|
-
? "
|
|
4099
|
-
: status ===
|
|
4100
|
-
? "
|
|
4101
|
-
:
|
|
4124
|
+
: status === 504
|
|
4125
|
+
? "upstream_timeout"
|
|
4126
|
+
: status === 429
|
|
4127
|
+
? "rate_limit_error"
|
|
4128
|
+
: status === 401
|
|
4129
|
+
? "authentication_error"
|
|
4130
|
+
: status === 403
|
|
4131
|
+
? "permission_error"
|
|
4132
|
+
: status === 400
|
|
4133
|
+
? "invalid_request_error"
|
|
4134
|
+
: "api_error",
|
|
4102
4135
|
};
|
|
4103
4136
|
invalidRequestFailure =
|
|
4104
4137
|
getCodexFallbackInvalidRequestFailure(fallbackErr) ?? undefined;
|
|
@@ -4135,7 +4168,7 @@ async function tryConfiguredClaudeFallbackChain(args) {
|
|
|
4135
4168
|
durationMs: Date.now() - fallbackStart,
|
|
4136
4169
|
});
|
|
4137
4170
|
lastFallbackError = `[${fallback.provider}/${fallback.model}] ${redactProviderErrorMessage(describeTransportError(fallbackErr))}`;
|
|
4138
|
-
if (ctx.abortSignal?.aborted) {
|
|
4171
|
+
if (ctx.abortSignal?.aborted || terminalFailure.retryable === false) {
|
|
4139
4172
|
break;
|
|
4140
4173
|
}
|
|
4141
4174
|
}
|
|
@@ -4151,8 +4184,12 @@ async function tryConfiguredClaudeFallbackChain(args) {
|
|
|
4151
4184
|
function buildConfiguredClaudeFallbackFailure(args) {
|
|
4152
4185
|
const { failure, buildLoggedClaudeError, tracer, requestStartTime } = args;
|
|
4153
4186
|
tracer?.setError(failure.errorType, failure.message);
|
|
4187
|
+
const body = buildLoggedClaudeError(failure.status, failure.message, failure.errorType, { errorCode: failure.errorCode, retryable: failure.retryable });
|
|
4154
4188
|
tracer?.end(failure.status, Date.now() - requestStartTime);
|
|
4155
|
-
|
|
4189
|
+
Object.assign(body.error, {
|
|
4190
|
+
code: failure.errorCode,
|
|
4191
|
+
retryable: failure.retryable,
|
|
4192
|
+
});
|
|
4156
4193
|
return new Response(JSON.stringify(body), {
|
|
4157
4194
|
status: failure.status,
|
|
4158
4195
|
headers: { "content-type": "application/json" },
|
|
@@ -4204,6 +4241,11 @@ async function tryAutoClaudeFallback(args) {
|
|
|
4204
4241
|
return { response };
|
|
4205
4242
|
}
|
|
4206
4243
|
catch (fallbackErr) {
|
|
4244
|
+
if (fallbackErr instanceof ProxyContextPreflightError ||
|
|
4245
|
+
getProxyTokenBudgetError(fallbackErr) ||
|
|
4246
|
+
ctx.abortSignal?.aborted) {
|
|
4247
|
+
throw fallbackErr;
|
|
4248
|
+
}
|
|
4207
4249
|
const errorMessage = redactProviderErrorMessage(describeTransportError(fallbackErr));
|
|
4208
4250
|
logger.always(`[proxy] fallback auto-provider failed: ${errorMessage}`);
|
|
4209
4251
|
recordFallbackAttempt({
|
|
@@ -4220,7 +4262,29 @@ async function tryAutoClaudeFallback(args) {
|
|
|
4220
4262
|
attemptCount: 1,
|
|
4221
4263
|
reason: "fallback_failure",
|
|
4222
4264
|
});
|
|
4223
|
-
|
|
4265
|
+
const failure = getProxyUpstreamFailure(fallbackErr);
|
|
4266
|
+
const status = failure?.status ?? 502;
|
|
4267
|
+
return {
|
|
4268
|
+
response: null,
|
|
4269
|
+
lastErrorMessage: errorMessage,
|
|
4270
|
+
terminalFailure: {
|
|
4271
|
+
status,
|
|
4272
|
+
message: sanitizeForLog(`Automatic fallback failed: ${failure?.message ?? errorMessage}`, 500),
|
|
4273
|
+
errorCode: failure?.code,
|
|
4274
|
+
retryable: failure?.retryable,
|
|
4275
|
+
errorType: status === 504
|
|
4276
|
+
? "upstream_timeout"
|
|
4277
|
+
: status === 429
|
|
4278
|
+
? "rate_limit_error"
|
|
4279
|
+
: status === 401
|
|
4280
|
+
? "authentication_error"
|
|
4281
|
+
: status === 403
|
|
4282
|
+
? "permission_error"
|
|
4283
|
+
: status === 400
|
|
4284
|
+
? "invalid_request_error"
|
|
4285
|
+
: "api_error",
|
|
4286
|
+
},
|
|
4287
|
+
};
|
|
4224
4288
|
}
|
|
4225
4289
|
}
|
|
4226
4290
|
/** Human-readable list that stays short when the pool is large. */
|
|
@@ -5258,31 +5322,29 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
5258
5322
|
headers.authorization = `Bearer ${account.token}`;
|
|
5259
5323
|
const retryAttemptNumber = allocateAttemptNumber();
|
|
5260
5324
|
const retryAttemptStartedAt = Date.now();
|
|
5261
|
-
recordAttempt(account.label, account.type);
|
|
5262
5325
|
const retryLogAttempt = (status, errorType, errorMessage, extra) => logAttempt(status, errorType, errorMessage, {
|
|
5263
5326
|
...extra,
|
|
5264
5327
|
attempt: retryAttemptNumber,
|
|
5265
5328
|
attemptDurationMs: extra?.attemptDurationMs ?? Date.now() - retryAttemptStartedAt,
|
|
5266
5329
|
});
|
|
5267
|
-
const
|
|
5330
|
+
const preparedRetryBody = buildUpstreamBody(account.token);
|
|
5268
5331
|
const retryFetchStartMs = Date.now();
|
|
5269
|
-
logProxyBody({
|
|
5270
|
-
phase: "upstream_request",
|
|
5271
|
-
headers,
|
|
5272
|
-
body: retryBodyStr,
|
|
5273
|
-
bodySize: Buffer.byteLength(retryBodyStr, "utf8"),
|
|
5274
|
-
contentType: headers["content-type"] ?? "application/json",
|
|
5275
|
-
account: account.label,
|
|
5276
|
-
accountType: account.type,
|
|
5277
|
-
attempt: retryAttemptNumber,
|
|
5278
|
-
metadata: { upstreamMethod: "POST", upstreamUrl: url },
|
|
5279
|
-
});
|
|
5280
5332
|
try {
|
|
5281
5333
|
const retryResp = await fetchAnthropicUpstream(url, {
|
|
5282
5334
|
method: "POST",
|
|
5283
5335
|
headers,
|
|
5284
|
-
body:
|
|
5285
|
-
signal: AbortSignal.
|
|
5336
|
+
body: preparedRetryBody.bodyStr,
|
|
5337
|
+
signal: AbortSignal.any([
|
|
5338
|
+
...(ctx.abortSignal ? [ctx.abortSignal] : []),
|
|
5339
|
+
AbortSignal.timeout(UPSTREAM_FETCH_TIMEOUT_MS),
|
|
5340
|
+
]),
|
|
5341
|
+
}, ctx, account.key, {
|
|
5342
|
+
logProxyBody,
|
|
5343
|
+
tracer,
|
|
5344
|
+
attemptNumber: retryAttemptNumber,
|
|
5345
|
+
accountLabel: account.label,
|
|
5346
|
+
accountType: account.type,
|
|
5347
|
+
preparedContext: preparedRetryBody.preparedContext,
|
|
5286
5348
|
});
|
|
5287
5349
|
if (retryResp.ok) {
|
|
5288
5350
|
authRetrySucceeded = true;
|
|
@@ -5299,7 +5361,7 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
5299
5361
|
requestStartTime,
|
|
5300
5362
|
fetchStartMs: retryFetchStartMs,
|
|
5301
5363
|
attemptNumber: retryAttemptNumber,
|
|
5302
|
-
finalBodyStr:
|
|
5364
|
+
finalBodyStr: preparedRetryBody.bodyStr,
|
|
5303
5365
|
upstreamSpan: currentUpstreamSpan,
|
|
5304
5366
|
logAttempt: retryLogAttempt,
|
|
5305
5367
|
logProxyBody,
|
|
@@ -5317,7 +5379,7 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
5317
5379
|
requestStartTime,
|
|
5318
5380
|
fetchStartMs: retryFetchStartMs,
|
|
5319
5381
|
attemptNumber: retryAttemptNumber,
|
|
5320
|
-
finalBodyStr:
|
|
5382
|
+
finalBodyStr: preparedRetryBody.bodyStr,
|
|
5321
5383
|
upstreamSpan: currentUpstreamSpan,
|
|
5322
5384
|
logAttempt: retryLogAttempt,
|
|
5323
5385
|
logProxyBody,
|
|
@@ -5527,6 +5589,16 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
5527
5589
|
}
|
|
5528
5590
|
}
|
|
5529
5591
|
catch (retryFetchErr) {
|
|
5592
|
+
if (retryFetchErr instanceof ProxyContextPreflightError ||
|
|
5593
|
+
getProxyTokenBudgetError(retryFetchErr)) {
|
|
5594
|
+
currentUpstreamSpan?.end();
|
|
5595
|
+
throw retryFetchErr;
|
|
5596
|
+
}
|
|
5597
|
+
if (ctx.abortSignal?.aborted) {
|
|
5598
|
+
retryLogAttempt(499, "client_cancelled", "Client cancelled upstream request", { retryable: false });
|
|
5599
|
+
currentUpstreamSpan?.end();
|
|
5600
|
+
throw retryFetchErr;
|
|
5601
|
+
}
|
|
5530
5602
|
currentSawNetworkError = true;
|
|
5531
5603
|
recordAttemptError(account.label, account.type, 502);
|
|
5532
5604
|
const message = retryFetchErr instanceof Error
|
|
@@ -5944,6 +6016,9 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
5944
6016
|
let tracer;
|
|
5945
6017
|
try {
|
|
5946
6018
|
tracer = ProxyTracer.startRequest({
|
|
6019
|
+
recordRequestMetrics: getProxyRequestAccounting(ctx.requestId)?.accountingScope !==
|
|
6020
|
+
"internal",
|
|
6021
|
+
recordUsageMetrics: true,
|
|
5947
6022
|
requestId: ctx.requestId,
|
|
5948
6023
|
method: ctx.method,
|
|
5949
6024
|
path: ctx.path,
|
|
@@ -5994,11 +6069,76 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
5994
6069
|
};
|
|
5995
6070
|
let finalRequestLogged = false;
|
|
5996
6071
|
const logFinalRequest = (status, accountLabel, accountType, errorType, errorMessage, extra) => {
|
|
5997
|
-
if (finalRequestLogged) {
|
|
6072
|
+
if (finalRequestLogged || isProxyRequestFinalized(ctx.requestId)) {
|
|
5998
6073
|
logger.debug(`[claude-proxy] ignored duplicate finalization for request ${ctx.requestId}`);
|
|
5999
6074
|
return;
|
|
6000
6075
|
}
|
|
6001
6076
|
finalRequestLogged = true;
|
|
6077
|
+
if (ctx.abortSignal?.aborted) {
|
|
6078
|
+
status = 499;
|
|
6079
|
+
errorType = "client_cancelled";
|
|
6080
|
+
errorMessage = "Client cancelled request";
|
|
6081
|
+
}
|
|
6082
|
+
const sdkFallbackFailure = ctx.metadata.sdkFallbackFailure;
|
|
6083
|
+
if (sdkFallbackFailure) {
|
|
6084
|
+
accountLabel = "unknown";
|
|
6085
|
+
accountType = "translation";
|
|
6086
|
+
tracer?.setServedAccount(accountLabel, accountType);
|
|
6087
|
+
if (sdkFallbackFailure.model) {
|
|
6088
|
+
tracer?.setModelSubstitution(body.model, sdkFallbackFailure.model, sdkFallbackFailure.provider);
|
|
6089
|
+
}
|
|
6090
|
+
if (sdkFallbackFailure.inputTokens !== undefined &&
|
|
6091
|
+
sdkFallbackFailure.outputTokens !== undefined) {
|
|
6092
|
+
tracer?.setUsage({
|
|
6093
|
+
inputTokens: sdkFallbackFailure.inputTokens,
|
|
6094
|
+
outputTokens: sdkFallbackFailure.outputTokens,
|
|
6095
|
+
cacheReadTokens: sdkFallbackFailure.cacheReadTokens ?? 0,
|
|
6096
|
+
cacheCreationTokens: sdkFallbackFailure.cacheCreationTokens ?? 0,
|
|
6097
|
+
inputIncludesCachedTokens: sdkFallbackFailure.inputIncludesCachedTokens,
|
|
6098
|
+
reasoningTokens: sdkFallbackFailure.reasoningTokens,
|
|
6099
|
+
});
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
6102
|
+
const fallbackFailureUsage = ctx.metadata
|
|
6103
|
+
.codexFallbackFailureUsage;
|
|
6104
|
+
if (fallbackFailureUsage) {
|
|
6105
|
+
extra = {
|
|
6106
|
+
inputTokens: fallbackFailureUsage.inputTokensObserved
|
|
6107
|
+
? fallbackFailureUsage.input
|
|
6108
|
+
: undefined,
|
|
6109
|
+
outputTokens: fallbackFailureUsage.outputTokensObserved
|
|
6110
|
+
? fallbackFailureUsage.output
|
|
6111
|
+
: undefined,
|
|
6112
|
+
cacheReadTokens: fallbackFailureUsage.cacheReadTokens,
|
|
6113
|
+
cacheCreationTokens: fallbackFailureUsage.cacheCreationTokens,
|
|
6114
|
+
reasoningTokens: fallbackFailureUsage.reasoning,
|
|
6115
|
+
...extra,
|
|
6116
|
+
};
|
|
6117
|
+
}
|
|
6118
|
+
if (!accountLabel &&
|
|
6119
|
+
typeof ctx.metadata.codexFallbackAccount === "string" &&
|
|
6120
|
+
ctx.metadata.codexFallbackAccount) {
|
|
6121
|
+
accountLabel = ctx.metadata.codexFallbackAccount;
|
|
6122
|
+
if (typeof ctx.metadata.codexFallbackAccountType === "string") {
|
|
6123
|
+
accountType = ctx.metadata.codexFallbackAccountType;
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
if (!sdkFallbackFailure &&
|
|
6127
|
+
ctx.metadata.codexFallbackDeferredFailure === true &&
|
|
6128
|
+
typeof ctx.metadata.codexFallbackModel === "string") {
|
|
6129
|
+
tracer?.setModelSubstitution(body.model, ctx.metadata.codexFallbackModel, "openai");
|
|
6130
|
+
tracer?.setServedAccount(accountLabel || "unknown", accountType);
|
|
6131
|
+
if (fallbackFailureUsage?.inputTokensObserved &&
|
|
6132
|
+
fallbackFailureUsage.outputTokensObserved) {
|
|
6133
|
+
tracer?.setUsage({
|
|
6134
|
+
inputTokens: fallbackFailureUsage.input,
|
|
6135
|
+
outputTokens: fallbackFailureUsage.output,
|
|
6136
|
+
cacheReadTokens: fallbackFailureUsage.cacheReadTokens ?? 0,
|
|
6137
|
+
cacheCreationTokens: fallbackFailureUsage.cacheCreationTokens ?? 0,
|
|
6138
|
+
reasoningTokens: fallbackFailureUsage.reasoning,
|
|
6139
|
+
}, { recordMetrics: false });
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
6002
6142
|
const finalAccountLabel = accountLabel ||
|
|
6003
6143
|
(status >= 400 ? PROXY_INTERNAL_ACCOUNT_LABEL : undefined);
|
|
6004
6144
|
const finalAccountType = accountLabel
|
|
@@ -6006,8 +6146,14 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
6006
6146
|
: status >= 400
|
|
6007
6147
|
? PROXY_INTERNAL_ACCOUNT_TYPE
|
|
6008
6148
|
: undefined;
|
|
6009
|
-
const finalAccountIdentity =
|
|
6010
|
-
|
|
6149
|
+
const finalAccountIdentity = sdkFallbackFailure
|
|
6150
|
+
? {
|
|
6151
|
+
accountKey: sdkFallbackFailure.accountKey,
|
|
6152
|
+
provider: sdkFallbackFailure.provider,
|
|
6153
|
+
}
|
|
6154
|
+
: resolveRequestLogAccountIdentity(finalAccountLabel, finalAccountType);
|
|
6155
|
+
const ownsClientOutcome = getProxyRequestAccounting(ctx.requestId)?.accountingScope !== "internal";
|
|
6156
|
+
if (ownsClientOutcome && status >= 400) {
|
|
6011
6157
|
recordFinalError(status, finalAccountLabel, finalAccountType, {
|
|
6012
6158
|
requestId: ctx.requestId,
|
|
6013
6159
|
...(finalAccountIdentity.accountKey
|
|
@@ -6023,7 +6169,7 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
6023
6169
|
errorCode: extra?.errorCode,
|
|
6024
6170
|
});
|
|
6025
6171
|
}
|
|
6026
|
-
else {
|
|
6172
|
+
else if (ownsClientOutcome) {
|
|
6027
6173
|
recordFinalSuccess(finalAccountLabel, finalAccountType);
|
|
6028
6174
|
}
|
|
6029
6175
|
const traceCtx = tracer?.getTraceContext();
|
|
@@ -6041,6 +6187,18 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
6041
6187
|
inputIncludesCachedTokens: false,
|
|
6042
6188
|
}
|
|
6043
6189
|
: {}),
|
|
6190
|
+
...(ctx.metadata.contextPreflight
|
|
6191
|
+
? {
|
|
6192
|
+
contextPreflight: ctx.metadata
|
|
6193
|
+
.contextPreflight,
|
|
6194
|
+
}
|
|
6195
|
+
: {}),
|
|
6196
|
+
...(ctx.metadata.tokenBudget
|
|
6197
|
+
? {
|
|
6198
|
+
tokenBudget: ctx.metadata
|
|
6199
|
+
.tokenBudget,
|
|
6200
|
+
}
|
|
6201
|
+
: {}),
|
|
6044
6202
|
stream: !!body.stream,
|
|
6045
6203
|
toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
|
|
6046
6204
|
account: finalAccountLabel ?? "",
|
|
@@ -6051,6 +6209,7 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
6051
6209
|
...(finalAccountIdentity.provider
|
|
6052
6210
|
? { provider: finalAccountIdentity.provider }
|
|
6053
6211
|
: {}),
|
|
6212
|
+
...sdkFallbackFailure,
|
|
6054
6213
|
...buildClientAttribution(ctx.headers),
|
|
6055
6214
|
responseStatus: status,
|
|
6056
6215
|
responseTimeMs: Date.now() - requestStartTime,
|
|
@@ -6058,6 +6217,7 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
6058
6217
|
...(errorType ? { errorType } : {}),
|
|
6059
6218
|
...(errorMessage ? { errorMessage } : {}),
|
|
6060
6219
|
...(extra?.errorCode ? { errorCode: extra.errorCode } : {}),
|
|
6220
|
+
...(extra?.retryable !== undefined ? { retryable: extra.retryable } : {}),
|
|
6061
6221
|
...(extra?.transportScope
|
|
6062
6222
|
? { transportScope: extra.transportScope }
|
|
6063
6223
|
: {}),
|
|
@@ -6131,6 +6291,18 @@ function createAnthropicAttemptLogger(args) {
|
|
|
6131
6291
|
method: ctx.method,
|
|
6132
6292
|
path: ctx.path,
|
|
6133
6293
|
model: body.model,
|
|
6294
|
+
...(ctx.metadata.contextPreflight
|
|
6295
|
+
? {
|
|
6296
|
+
contextPreflight: ctx.metadata
|
|
6297
|
+
.contextPreflight,
|
|
6298
|
+
}
|
|
6299
|
+
: {}),
|
|
6300
|
+
...(ctx.metadata.tokenBudget
|
|
6301
|
+
? {
|
|
6302
|
+
tokenBudget: ctx.metadata
|
|
6303
|
+
.tokenBudget,
|
|
6304
|
+
}
|
|
6305
|
+
: {}),
|
|
6134
6306
|
stream: !!body.stream,
|
|
6135
6307
|
toolCount,
|
|
6136
6308
|
account: account.label,
|
|
@@ -6175,7 +6347,7 @@ function createAnthropicAttemptLogger(args) {
|
|
|
6175
6347
|
};
|
|
6176
6348
|
}
|
|
6177
6349
|
async function prepareAnthropicAccountAttempt(args) {
|
|
6178
|
-
const { account, accountState, bodyStr, clientHeaders, isClaudeClientRequest, url, tracer, attemptNumber, currentLastError, currentAuthFailureMessage, logAttempt, logProxyBody, } = args;
|
|
6350
|
+
const { ctx, account, accountState, bodyStr, clientHeaders, isClaudeClientRequest, url, tracer, attemptNumber, currentLastError, currentAuthFailureMessage, logAttempt, logProxyBody: _logProxyBody, } = args;
|
|
6179
6351
|
let lastError = currentLastError;
|
|
6180
6352
|
let authFailureMessage = currentAuthFailureMessage;
|
|
6181
6353
|
if (needsRefresh(account)) {
|
|
@@ -6276,9 +6448,12 @@ async function prepareAnthropicAccountAttempt(args) {
|
|
|
6276
6448
|
delete headers["anthropic-beta"];
|
|
6277
6449
|
}
|
|
6278
6450
|
}
|
|
6279
|
-
const buildUpstreamBody = (token) =>
|
|
6280
|
-
|
|
6281
|
-
|
|
6451
|
+
const buildUpstreamBody = (token) => {
|
|
6452
|
+
const built = isOAuth
|
|
6453
|
+
? polyfillOAuthBody(bodyStr, token, snapshot, isClaudeClientRequest, headers["x-claude-code-session-id"])
|
|
6454
|
+
: { bodyStr };
|
|
6455
|
+
return { ...built, ...prepareAnthropicWire(ctx, built.bodyStr) };
|
|
6456
|
+
};
|
|
6282
6457
|
const polyfilledBody = buildUpstreamBody(account.token);
|
|
6283
6458
|
if (isOAuth &&
|
|
6284
6459
|
polyfilledBody.sessionId &&
|
|
@@ -6287,7 +6462,6 @@ async function prepareAnthropicAccountAttempt(args) {
|
|
|
6287
6462
|
}
|
|
6288
6463
|
const finalBodyStr = polyfilledBody.bodyStr;
|
|
6289
6464
|
logger.always(`[proxy] → account=${account.label} (${account.type})`);
|
|
6290
|
-
recordAttempt(account.label, account.type);
|
|
6291
6465
|
const fetchStartMs = Date.now();
|
|
6292
6466
|
let upstreamSpan;
|
|
6293
6467
|
if (tracer) {
|
|
@@ -6298,21 +6472,8 @@ async function prepareAnthropicAccountAttempt(args) {
|
|
|
6298
6472
|
polyfillBody: isOAuth,
|
|
6299
6473
|
upstreamUrl: url,
|
|
6300
6474
|
});
|
|
6301
|
-
tracer.logUpstreamRequestHeaders(headers);
|
|
6302
|
-
tracer.logUpstreamRequestBody(finalBodyStr);
|
|
6303
6475
|
Object.assign(headers, tracer.getTraceHeaders());
|
|
6304
6476
|
}
|
|
6305
|
-
logProxyBody({
|
|
6306
|
-
phase: "upstream_request",
|
|
6307
|
-
headers,
|
|
6308
|
-
body: finalBodyStr,
|
|
6309
|
-
bodySize: Buffer.byteLength(finalBodyStr, "utf8"),
|
|
6310
|
-
contentType: headers["content-type"] ?? "application/json",
|
|
6311
|
-
account: account.label,
|
|
6312
|
-
accountType: account.type,
|
|
6313
|
-
attempt: attemptNumber,
|
|
6314
|
-
metadata: { upstreamMethod: "POST", upstreamUrl: url },
|
|
6315
|
-
});
|
|
6316
6477
|
return {
|
|
6317
6478
|
continueLoop: false,
|
|
6318
6479
|
lastError,
|
|
@@ -6320,6 +6481,7 @@ async function prepareAnthropicAccountAttempt(args) {
|
|
|
6320
6481
|
headers,
|
|
6321
6482
|
buildUpstreamBody,
|
|
6322
6483
|
finalBodyStr,
|
|
6484
|
+
preparedContext: polyfilledBody.preparedContext,
|
|
6323
6485
|
fetchStartMs,
|
|
6324
6486
|
upstreamSpan,
|
|
6325
6487
|
};
|
|
@@ -6351,7 +6513,7 @@ function buildAnthropicConstructionRejectionTerminalError() {
|
|
|
6351
6513
|
};
|
|
6352
6514
|
}
|
|
6353
6515
|
async function fetchAnthropicAccountResponse(args) {
|
|
6354
|
-
const { url, headers, finalBodyStr, requestedModel, account, accountState: _accountState2, enabledAccounts: _enabledAccounts, orderedAccounts: _orderedAccounts, tracer, logAttempt, logProxyBody, fetchStartMs, attemptNumber, currentLastError, currentSawRateLimit, currentSawNetworkError, upstreamSpan, } = args;
|
|
6516
|
+
const { ctx, url, headers, finalBodyStr, preparedContext, requestedModel, account, accountState: _accountState2, enabledAccounts: _enabledAccounts, orderedAccounts: _orderedAccounts, tracer, logAttempt, logProxyBody, fetchStartMs, attemptNumber, currentLastError, currentSawRateLimit, currentSawNetworkError, upstreamSpan, } = args;
|
|
6355
6517
|
let lastError = currentLastError;
|
|
6356
6518
|
let sawRateLimit = currentSawRateLimit;
|
|
6357
6519
|
let sawNetworkError = currentSawNetworkError;
|
|
@@ -6362,10 +6524,32 @@ async function fetchAnthropicAccountResponse(args) {
|
|
|
6362
6524
|
method: "POST",
|
|
6363
6525
|
headers,
|
|
6364
6526
|
body: finalBodyStr,
|
|
6365
|
-
signal: AbortSignal.
|
|
6527
|
+
signal: AbortSignal.any([
|
|
6528
|
+
...(ctx.abortSignal ? [ctx.abortSignal] : []),
|
|
6529
|
+
AbortSignal.timeout(UPSTREAM_FETCH_TIMEOUT_MS),
|
|
6530
|
+
]),
|
|
6531
|
+
}, ctx, account.key, {
|
|
6532
|
+
logProxyBody,
|
|
6533
|
+
tracer,
|
|
6534
|
+
attemptNumber,
|
|
6535
|
+
accountLabel: account.label,
|
|
6536
|
+
accountType: account.type,
|
|
6537
|
+
preparedContext,
|
|
6366
6538
|
});
|
|
6367
6539
|
}
|
|
6368
6540
|
catch (fetchErr) {
|
|
6541
|
+
if (fetchErr instanceof ProxyContextPreflightError ||
|
|
6542
|
+
getProxyTokenBudgetError(fetchErr)) {
|
|
6543
|
+
currentUpstreamSpan?.end();
|
|
6544
|
+
throw fetchErr;
|
|
6545
|
+
}
|
|
6546
|
+
if (ctx.abortSignal?.aborted) {
|
|
6547
|
+
logAttempt(499, "client_cancelled", "Client cancelled upstream request", {
|
|
6548
|
+
retryable: false,
|
|
6549
|
+
});
|
|
6550
|
+
currentUpstreamSpan?.end();
|
|
6551
|
+
throw ctx.abortSignal.reason;
|
|
6552
|
+
}
|
|
6369
6553
|
const retryable = isRetryableNetworkError(fetchErr);
|
|
6370
6554
|
const connectPhase = isConnectPhaseNetworkError(fetchErr);
|
|
6371
6555
|
const transportScope = classifyNetworkTransportScope(fetchErr);
|
|
@@ -6657,399 +6841,408 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
6657
6841
|
// When every eligible account is busy, reserve the first account that frees
|
|
6658
6842
|
// instead of arbitrarily waiting behind the last configured account.
|
|
6659
6843
|
let queuedAccountAdmission;
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
transientSameAccountRetries
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
loopState.attemptNumber += 1;
|
|
6695
|
-
if (tracer && loopState.attemptNumber === 1 && acctSelectionSpan) {
|
|
6696
|
-
tracer.setAccountSelection({
|
|
6697
|
-
strategy: accountStrategy,
|
|
6698
|
-
accountsTotal: accounts.length,
|
|
6699
|
-
accountsHealthy: enabledAccounts.length,
|
|
6700
|
-
selectedAccount: account.label,
|
|
6701
|
-
accountType: account.type,
|
|
6702
|
-
});
|
|
6703
|
-
acctSelectionSpan.end();
|
|
6704
|
-
}
|
|
6705
|
-
const logAttempt = createAnthropicAttemptLogger({
|
|
6706
|
-
ctx,
|
|
6707
|
-
body,
|
|
6708
|
-
toolCount,
|
|
6709
|
-
requestStart,
|
|
6710
|
-
tracer,
|
|
6711
|
-
account,
|
|
6712
|
-
attemptNumber: loopState.attemptNumber,
|
|
6713
|
-
});
|
|
6714
|
-
const preparedAttempt = await prepareAnthropicAccountAttempt({
|
|
6715
|
-
account,
|
|
6716
|
-
accountState,
|
|
6717
|
-
bodyStr,
|
|
6718
|
-
clientHeaders,
|
|
6719
|
-
isClaudeClientRequest,
|
|
6720
|
-
url,
|
|
6721
|
-
tracer,
|
|
6722
|
-
attemptNumber: loopState.attemptNumber,
|
|
6723
|
-
currentLastError: loopState.lastError,
|
|
6724
|
-
currentAuthFailureMessage: loopState.authFailureMessage,
|
|
6725
|
-
logAttempt,
|
|
6726
|
-
logProxyBody,
|
|
6727
|
-
});
|
|
6728
|
-
loopState.lastError = preparedAttempt.lastError;
|
|
6729
|
-
loopState.authFailureMessage = preparedAttempt.authFailureMessage;
|
|
6730
|
-
if (preparedAttempt.continueLoop ||
|
|
6731
|
-
!preparedAttempt.headers ||
|
|
6732
|
-
!preparedAttempt.buildUpstreamBody ||
|
|
6733
|
-
!preparedAttempt.finalBodyStr ||
|
|
6734
|
-
preparedAttempt.fetchStartMs === undefined) {
|
|
6735
|
-
if (transportPermit.probe) {
|
|
6736
|
-
providerTransportCoordinator.reportProbeAbandoned(transportPermit);
|
|
6737
|
-
}
|
|
6738
|
-
continue accountLoop;
|
|
6739
|
-
}
|
|
6740
|
-
let admissionLease;
|
|
6741
|
-
if (queuedAccountAdmission?.accountKey === account.key) {
|
|
6742
|
-
admissionLease = queuedAccountAdmission.lease;
|
|
6743
|
-
queuedAccountAdmission = undefined;
|
|
6744
|
-
}
|
|
6745
|
-
else {
|
|
6746
|
-
admissionLease = tryAcquireAccountAdmission(account.key, accountAdmissionCapacity);
|
|
6747
|
-
if (admissionLease && queuedAccountAdmission) {
|
|
6748
|
-
// A preferred account became available while the race was being
|
|
6749
|
-
// established; release the lower-priority reservation.
|
|
6750
|
-
queuedAccountAdmission.lease.release();
|
|
6751
|
-
queuedAccountAdmission = undefined;
|
|
6752
|
-
}
|
|
6753
|
-
}
|
|
6754
|
-
if (!admissionLease) {
|
|
6755
|
-
// A later account may be immediately available. Preserve the routing
|
|
6756
|
-
// order but do not leave a request queued behind a busy first choice.
|
|
6757
|
-
if (transportPermit.probe) {
|
|
6758
|
-
providerTransportCoordinator.reportProbeAbandoned(transportPermit);
|
|
6844
|
+
try {
|
|
6845
|
+
if (accountAdmissionCapacity !== undefined &&
|
|
6846
|
+
effectiveAccounts.length > 0 &&
|
|
6847
|
+
effectiveAccounts.every((account) => !isAccountAdmissionAvailable(account.key, accountAdmissionCapacity))) {
|
|
6848
|
+
queuedAccountAdmission = await acquireFirstAvailableAccountAdmission(effectiveAccounts.map((account) => account.key), accountAdmissionCapacity, ctx.abortSignal);
|
|
6849
|
+
}
|
|
6850
|
+
accountLoop: for (const [accountIndex, account,] of effectiveAccounts.entries()) {
|
|
6851
|
+
const hasNextAccount = accountIndex < effectiveAccounts.length - 1;
|
|
6852
|
+
const accountState = getOrCreateRuntimeState(account.key);
|
|
6853
|
+
let transientSameAccountRetries = 0;
|
|
6854
|
+
let rateLimitSameAccountRetries = 0;
|
|
6855
|
+
while (true) {
|
|
6856
|
+
const transportPermit = await providerTransportCoordinator.acquire(ctx.abortSignal);
|
|
6857
|
+
if (transportPermit.allowed === false) {
|
|
6858
|
+
loopState.sawNetworkError = true;
|
|
6859
|
+
loopState.lastTransportErrorCode =
|
|
6860
|
+
transportPermit.errorCode ?? undefined;
|
|
6861
|
+
loopState.lastTransportScope = transportPermit.transportScope;
|
|
6862
|
+
loopState.lastError = `Anthropic transport recovery probe failed (${transportPermit.errorCode ?? "unknown"})`;
|
|
6863
|
+
// The probe that failed was another request's attempt; this one has
|
|
6864
|
+
// sent nothing. Give it the same bounded same-account budget a direct
|
|
6865
|
+
// transport failure gets, instead of failing every queued request the
|
|
6866
|
+
// moment one probe times out.
|
|
6867
|
+
const probeRetryBudget = transportPermit.connectPhase
|
|
6868
|
+
? MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES
|
|
6869
|
+
: MAX_TRANSIENT_SAME_ACCOUNT_RETRIES;
|
|
6870
|
+
if (transientSameAccountRetries < probeRetryBudget) {
|
|
6871
|
+
transientSameAccountRetries += 1;
|
|
6872
|
+
const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
|
|
6873
|
+
logger.always(`[proxy] retrying same account=${account.label} after failed transport recovery probe (${transientSameAccountRetries}/${probeRetryBudget}) in ${delayMs}ms`);
|
|
6874
|
+
await sleep(delayMs);
|
|
6875
|
+
continue;
|
|
6876
|
+
}
|
|
6877
|
+
break accountLoop;
|
|
6759
6878
|
}
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
headers: preparedAttempt.headers,
|
|
6769
|
-
finalBodyStr: preparedAttempt.finalBodyStr,
|
|
6770
|
-
requestedModel: body.model,
|
|
6771
|
-
account,
|
|
6772
|
-
accountState,
|
|
6773
|
-
enabledAccounts,
|
|
6774
|
-
orderedAccounts,
|
|
6775
|
-
tracer,
|
|
6776
|
-
logAttempt,
|
|
6777
|
-
logProxyBody,
|
|
6778
|
-
fetchStartMs: preparedAttempt.fetchStartMs,
|
|
6779
|
-
attemptNumber: loopState.attemptNumber,
|
|
6780
|
-
currentLastError: loopState.lastError,
|
|
6781
|
-
currentSawRateLimit: loopState.sawRateLimit,
|
|
6782
|
-
currentSawNetworkError: loopState.sawNetworkError,
|
|
6783
|
-
upstreamSpan: preparedAttempt.upstreamSpan,
|
|
6879
|
+
loopState.attemptNumber += 1;
|
|
6880
|
+
if (tracer && loopState.attemptNumber === 1 && acctSelectionSpan) {
|
|
6881
|
+
tracer.setAccountSelection({
|
|
6882
|
+
strategy: accountStrategy,
|
|
6883
|
+
accountsTotal: accounts.length,
|
|
6884
|
+
accountsHealthy: enabledAccounts.length,
|
|
6885
|
+
selectedAccount: account.label,
|
|
6886
|
+
accountType: account.type,
|
|
6784
6887
|
});
|
|
6888
|
+
acctSelectionSpan.end();
|
|
6785
6889
|
}
|
|
6786
|
-
|
|
6890
|
+
const logAttempt = createAnthropicAttemptLogger({
|
|
6891
|
+
ctx,
|
|
6892
|
+
body,
|
|
6893
|
+
toolCount,
|
|
6894
|
+
requestStart,
|
|
6895
|
+
tracer,
|
|
6896
|
+
account,
|
|
6897
|
+
attemptNumber: loopState.attemptNumber,
|
|
6898
|
+
});
|
|
6899
|
+
const preparedAttempt = await prepareAnthropicAccountAttempt({
|
|
6900
|
+
ctx,
|
|
6901
|
+
account,
|
|
6902
|
+
accountState,
|
|
6903
|
+
bodyStr,
|
|
6904
|
+
clientHeaders,
|
|
6905
|
+
isClaudeClientRequest,
|
|
6906
|
+
url,
|
|
6907
|
+
tracer,
|
|
6908
|
+
attemptNumber: loopState.attemptNumber,
|
|
6909
|
+
currentLastError: loopState.lastError,
|
|
6910
|
+
currentAuthFailureMessage: loopState.authFailureMessage,
|
|
6911
|
+
logAttempt,
|
|
6912
|
+
logProxyBody,
|
|
6913
|
+
});
|
|
6914
|
+
loopState.lastError = preparedAttempt.lastError;
|
|
6915
|
+
loopState.authFailureMessage = preparedAttempt.authFailureMessage;
|
|
6916
|
+
if (preparedAttempt.continueLoop ||
|
|
6917
|
+
!preparedAttempt.headers ||
|
|
6918
|
+
!preparedAttempt.buildUpstreamBody ||
|
|
6919
|
+
!preparedAttempt.finalBodyStr ||
|
|
6920
|
+
!preparedAttempt.preparedContext ||
|
|
6921
|
+
preparedAttempt.fetchStartMs === undefined) {
|
|
6787
6922
|
if (transportPermit.probe) {
|
|
6788
6923
|
providerTransportCoordinator.reportProbeAbandoned(transportPermit);
|
|
6789
6924
|
}
|
|
6790
|
-
|
|
6925
|
+
continue accountLoop;
|
|
6791
6926
|
}
|
|
6792
|
-
|
|
6793
|
-
|
|
6927
|
+
let admissionLease;
|
|
6928
|
+
if (queuedAccountAdmission?.accountKey === account.key) {
|
|
6929
|
+
admissionLease = queuedAccountAdmission.lease;
|
|
6930
|
+
queuedAccountAdmission = undefined;
|
|
6794
6931
|
}
|
|
6795
6932
|
else {
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
loopState.lastTransportErrorCode = fetchResult.errorCode;
|
|
6933
|
+
admissionLease = tryAcquireAccountAdmission(account.key, accountAdmissionCapacity);
|
|
6934
|
+
if (admissionLease && queuedAccountAdmission) {
|
|
6935
|
+
// A preferred account became available while the race was being
|
|
6936
|
+
// established; release the lower-priority reservation.
|
|
6937
|
+
queuedAccountAdmission.lease.release();
|
|
6938
|
+
queuedAccountAdmission = undefined;
|
|
6939
|
+
}
|
|
6804
6940
|
}
|
|
6805
|
-
if (
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
logProxyBody,
|
|
6813
|
-
logFinalRequest,
|
|
6814
|
-
});
|
|
6941
|
+
if (!admissionLease) {
|
|
6942
|
+
// A later account may be immediately available. Preserve the routing
|
|
6943
|
+
// order but do not leave a request queued behind a busy first choice.
|
|
6944
|
+
if (transportPermit.probe) {
|
|
6945
|
+
providerTransportCoordinator.reportProbeAbandoned(transportPermit);
|
|
6946
|
+
}
|
|
6947
|
+
continue accountLoop;
|
|
6815
6948
|
}
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6949
|
+
let admissionTransferredToStream = false;
|
|
6950
|
+
try {
|
|
6951
|
+
let fetchResult;
|
|
6952
|
+
try {
|
|
6953
|
+
fetchResult = await fetchAnthropicAccountResponse({
|
|
6954
|
+
ctx,
|
|
6955
|
+
url,
|
|
6956
|
+
headers: preparedAttempt.headers,
|
|
6957
|
+
finalBodyStr: preparedAttempt.finalBodyStr,
|
|
6958
|
+
preparedContext: preparedAttempt.preparedContext,
|
|
6959
|
+
requestedModel: body.model,
|
|
6960
|
+
account,
|
|
6961
|
+
accountState,
|
|
6962
|
+
enabledAccounts,
|
|
6963
|
+
orderedAccounts,
|
|
6964
|
+
tracer,
|
|
6965
|
+
logAttempt,
|
|
6966
|
+
logProxyBody,
|
|
6967
|
+
fetchStartMs: preparedAttempt.fetchStartMs,
|
|
6968
|
+
attemptNumber: loopState.attemptNumber,
|
|
6969
|
+
currentLastError: loopState.lastError,
|
|
6970
|
+
currentSawRateLimit: loopState.sawRateLimit,
|
|
6971
|
+
currentSawNetworkError: loopState.sawNetworkError,
|
|
6972
|
+
upstreamSpan: preparedAttempt.upstreamSpan,
|
|
6973
|
+
});
|
|
6974
|
+
}
|
|
6975
|
+
catch (error) {
|
|
6976
|
+
if (transportPermit.probe) {
|
|
6977
|
+
providerTransportCoordinator.reportProbeAbandoned(transportPermit);
|
|
6836
6978
|
}
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6979
|
+
throw error;
|
|
6980
|
+
}
|
|
6981
|
+
if (fetchResult.transportScope) {
|
|
6982
|
+
providerTransportCoordinator.reportTransportFailure(fetchResult.errorCode, fetchResult.transportScope, transportPermit, fetchResult.connectPhase === true);
|
|
6983
|
+
}
|
|
6984
|
+
else {
|
|
6985
|
+
providerTransportCoordinator.reportSuccess(transportPermit);
|
|
6986
|
+
}
|
|
6987
|
+
loopState.lastError = fetchResult.lastError;
|
|
6988
|
+
loopState.sawRateLimit = fetchResult.sawRateLimit;
|
|
6989
|
+
loopState.sawNetworkError = fetchResult.sawNetworkError;
|
|
6990
|
+
if (fetchResult.transportScope) {
|
|
6991
|
+
loopState.lastTransportScope = fetchResult.transportScope;
|
|
6992
|
+
loopState.lastTransportErrorCode = fetchResult.errorCode;
|
|
6993
|
+
}
|
|
6994
|
+
if (fetchResult.terminalError) {
|
|
6995
|
+
return finalizeAnthropicTerminalFetchError({
|
|
6996
|
+
terminalError: fetchResult.terminalError,
|
|
6997
|
+
account,
|
|
6998
|
+
tracer,
|
|
6999
|
+
requestStartTime,
|
|
7000
|
+
attemptNumber: loopState.attemptNumber,
|
|
7001
|
+
logProxyBody,
|
|
7002
|
+
logFinalRequest,
|
|
7003
|
+
});
|
|
7004
|
+
}
|
|
7005
|
+
if (fetchResult.continueLoop || !fetchResult.response) {
|
|
7006
|
+
// Genuine 429 (carries a cooldown plan derived from quota headers).
|
|
7007
|
+
if (fetchResult.cooldownPlan) {
|
|
7008
|
+
const plan = fetchResult.cooldownPlan;
|
|
7009
|
+
// Refresh the account's quota snapshot for proactive selection.
|
|
7010
|
+
if (fetchResult.quota) {
|
|
7011
|
+
accountState.quota = mergeQuotaSnapshot(accountState.quota, fetchResult.quota);
|
|
7012
|
+
saveAccountQuota(account.key, fetchResult.quota).catch(() => {
|
|
7013
|
+
// Non-fatal: routing already has the in-memory snapshot.
|
|
7014
|
+
});
|
|
7015
|
+
}
|
|
7016
|
+
// Publish the cooldown before retrying so requests arriving behind
|
|
7017
|
+
// this one skip the throttled account instead of joining the burst.
|
|
7018
|
+
let cooldownExtended = false;
|
|
7019
|
+
if (plan.scope === "account" &&
|
|
7020
|
+
(!accountState.coolingUntil ||
|
|
7021
|
+
plan.coolingUntil > accountState.coolingUntil)) {
|
|
7022
|
+
accountState.coolingUntil = plan.coolingUntil;
|
|
7023
|
+
accountState.coolingReason = plan.reason;
|
|
7024
|
+
cooldownExtended = true;
|
|
7025
|
+
}
|
|
7026
|
+
if (cooldownExtended && plan.scope === "account") {
|
|
7027
|
+
await saveAccountCooldown(account.key, accountState.coolingUntil ?? plan.coolingUntil, accountState.coolingReason ?? plan.reason).catch(() => {
|
|
7028
|
+
// Non-fatal: routing already has the in-memory cooldown.
|
|
7029
|
+
});
|
|
7030
|
+
}
|
|
7031
|
+
// Transient retries are budgeted across all concurrent requests for
|
|
7032
|
+
// this account/window. Exhaustion plans rotate immediately and never
|
|
7033
|
+
// claim this budget.
|
|
7034
|
+
const sharedRetrySlot = fetchResult.retrySameAccount
|
|
7035
|
+
? claimTransientRateLimitRetry(account.key, plan.coolingUntil)
|
|
7036
|
+
: undefined;
|
|
7037
|
+
if (fetchResult.retrySameAccount &&
|
|
7038
|
+
fetchResult.retryAfterMs !== undefined &&
|
|
7039
|
+
rateLimitSameAccountRetries <
|
|
7040
|
+
MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES &&
|
|
7041
|
+
sharedRetrySlot !== undefined) {
|
|
7042
|
+
rateLimitSameAccountRetries += 1;
|
|
7043
|
+
const base = Math.min(fetchResult.retryAfterMs || 1_000, MAX_RATE_LIMIT_RETRY_DELAY_MS);
|
|
7044
|
+
// Stagger the two shared slots, then cap after jitter so the final
|
|
7045
|
+
// sleep never exceeds the configured maximum.
|
|
7046
|
+
const delayMs = Math.min(MAX_RATE_LIMIT_RETRY_DELAY_MS, jitteredDelay(base * sharedRetrySlot));
|
|
7047
|
+
logger.always(`[proxy] retrying same account=${account.label} after transient 429 (shared slot ${sharedRetrySlot}/${MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
|
|
7048
|
+
await sleep(delayMs);
|
|
7049
|
+
continue;
|
|
7050
|
+
}
|
|
7051
|
+
// Exhaustion, or the shared transient retry budget being used up:
|
|
7052
|
+
// rotate while the already-published cooldown remains active.
|
|
7053
|
+
advancePrimaryIfCurrent(account.key, enabledAccounts.length, orderedAccounts[0]?.key);
|
|
7054
|
+
logger.always(`[proxy] account=${account.label} rate-limited (${plan.reason}); cooling ~${minutesUntil(plan.coolingUntil, Date.now())}m until ${new Date(plan.coolingUntil).toISOString()}, rotating`);
|
|
7055
|
+
if (plan.rotateImmediately) {
|
|
7056
|
+
scheduleHandoffQuotaRefresh(account, effectiveAccounts[accountIndex + 1], plan.coolingUntil, sessionSoftLimit);
|
|
7057
|
+
}
|
|
7058
|
+
continue accountLoop;
|
|
6841
7059
|
}
|
|
6842
|
-
// Transient
|
|
6843
|
-
//
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
: undefined;
|
|
7060
|
+
// Transient error retry (network errors, 529 overloaded). A failure
|
|
7061
|
+
// from the connect phase sent nothing, so it earns the larger budget.
|
|
7062
|
+
const sameAccountRetryBudget = fetchResult.connectPhase
|
|
7063
|
+
? MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES
|
|
7064
|
+
: MAX_TRANSIENT_SAME_ACCOUNT_RETRIES;
|
|
6848
7065
|
if (fetchResult.retrySameAccount &&
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
rateLimitSameAccountRetries += 1;
|
|
6854
|
-
const base = Math.min(fetchResult.retryAfterMs || 1_000, MAX_RATE_LIMIT_RETRY_DELAY_MS);
|
|
6855
|
-
// Stagger the two shared slots, then cap after jitter so the final
|
|
6856
|
-
// sleep never exceeds the configured maximum.
|
|
6857
|
-
const delayMs = Math.min(MAX_RATE_LIMIT_RETRY_DELAY_MS, jitteredDelay(base * sharedRetrySlot));
|
|
6858
|
-
logger.always(`[proxy] retrying same account=${account.label} after transient 429 (shared slot ${sharedRetrySlot}/${MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
|
|
7066
|
+
transientSameAccountRetries < sameAccountRetryBudget) {
|
|
7067
|
+
transientSameAccountRetries += 1;
|
|
7068
|
+
const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
|
|
7069
|
+
logger.always(`[proxy] retrying same account=${account.label} after transient network error (${transientSameAccountRetries}/${sameAccountRetryBudget}) in ${delayMs}ms`);
|
|
6859
7070
|
await sleep(delayMs);
|
|
6860
7071
|
continue;
|
|
6861
7072
|
}
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
7073
|
+
if (fetchResult.retrySameAccount && !fetchResult.transportScope) {
|
|
7074
|
+
logger.always(`[proxy] exhausted transient same-account retries for account=${account.label}; rotating`);
|
|
7075
|
+
}
|
|
7076
|
+
if (fetchResult.transportScope) {
|
|
7077
|
+
logger.always(`[proxy] Anthropic ${fetchResult.transportScope} failure code=${fetchResult.errorCode ?? "unknown"}; suppressing cross-account rotation after ${transientSameAccountRetries + 1} attempts`);
|
|
7078
|
+
break accountLoop;
|
|
6868
7079
|
}
|
|
6869
7080
|
continue accountLoop;
|
|
6870
7081
|
}
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
7082
|
+
let upstreamSpan = fetchResult.upstreamSpan;
|
|
7083
|
+
const response = fetchResult.response;
|
|
7084
|
+
if (response.status === 401 &&
|
|
7085
|
+
account.type === "oauth" &&
|
|
7086
|
+
account.refreshToken) {
|
|
7087
|
+
const authRetryResult = await handleAnthropicAuthRetry({
|
|
7088
|
+
ctx,
|
|
7089
|
+
body,
|
|
7090
|
+
account,
|
|
7091
|
+
accountState,
|
|
7092
|
+
headers: preparedAttempt.headers,
|
|
7093
|
+
buildUpstreamBody: preparedAttempt.buildUpstreamBody,
|
|
7094
|
+
url,
|
|
7095
|
+
enabledAccounts,
|
|
7096
|
+
orderedAccounts,
|
|
7097
|
+
tracer,
|
|
7098
|
+
requestStartTime,
|
|
7099
|
+
allocateAttemptNumber: () => {
|
|
7100
|
+
loopState.attemptNumber += 1;
|
|
7101
|
+
return loopState.attemptNumber;
|
|
7102
|
+
},
|
|
7103
|
+
upstreamSpan,
|
|
7104
|
+
logAttempt,
|
|
7105
|
+
logProxyBody,
|
|
7106
|
+
logFinalRequest,
|
|
7107
|
+
onStreamTerminal: admissionLease.release,
|
|
7108
|
+
lastError: loopState.lastError,
|
|
7109
|
+
authFailureMessage: loopState.authFailureMessage,
|
|
7110
|
+
entitlementFailure: loopState.entitlementFailure,
|
|
7111
|
+
sawRateLimit: loopState.sawRateLimit,
|
|
7112
|
+
sawTransientFailure: loopState.sawTransientFailure,
|
|
7113
|
+
sawNetworkError: loopState.sawNetworkError,
|
|
7114
|
+
});
|
|
7115
|
+
loopState.lastError = authRetryResult.lastError;
|
|
7116
|
+
loopState.authFailureMessage = authRetryResult.authFailureMessage;
|
|
7117
|
+
loopState.entitlementFailure = authRetryResult.entitlementFailure;
|
|
7118
|
+
loopState.sawRateLimit = authRetryResult.sawRateLimit;
|
|
7119
|
+
loopState.sawTransientFailure = authRetryResult.sawTransientFailure;
|
|
7120
|
+
loopState.sawNetworkError = authRetryResult.sawNetworkError;
|
|
7121
|
+
upstreamSpan = authRetryResult.upstreamSpan;
|
|
7122
|
+
if (authRetryResult.response !== undefined) {
|
|
7123
|
+
admissionTransferredToStream =
|
|
7124
|
+
authRetryResult.holdsAccountAdmission === true;
|
|
7125
|
+
return authRetryResult.response;
|
|
7126
|
+
}
|
|
7127
|
+
if (authRetryResult.continueLoop) {
|
|
7128
|
+
if (hasNextAccount && authRetryResult.retryDelayMs) {
|
|
7129
|
+
logger.always(`[proxy] pacing cross-account SSE overload rotation for ${authRetryResult.retryDelayMs}ms after auth refresh`);
|
|
7130
|
+
await sleep(authRetryResult.retryDelayMs);
|
|
7131
|
+
}
|
|
7132
|
+
continue accountLoop;
|
|
7133
|
+
}
|
|
6886
7134
|
}
|
|
6887
|
-
if (
|
|
6888
|
-
|
|
7135
|
+
if (!response.ok) {
|
|
7136
|
+
const nonOkResult = await handleAnthropicNonOkResponse({
|
|
7137
|
+
response,
|
|
7138
|
+
account,
|
|
7139
|
+
accountState,
|
|
7140
|
+
enabledAccounts,
|
|
7141
|
+
orderedAccounts,
|
|
7142
|
+
tracer,
|
|
7143
|
+
requestStartTime,
|
|
7144
|
+
fetchStartMs: preparedAttempt.fetchStartMs,
|
|
7145
|
+
attemptNumber: loopState.attemptNumber,
|
|
7146
|
+
logAttempt,
|
|
7147
|
+
logProxyBody,
|
|
7148
|
+
logFinalRequest,
|
|
7149
|
+
lastError: loopState.lastError,
|
|
7150
|
+
authFailureMessage: loopState.authFailureMessage,
|
|
7151
|
+
sawTransientFailure: loopState.sawTransientFailure,
|
|
7152
|
+
invalidRequestFailure: loopState.invalidRequestFailure,
|
|
7153
|
+
entitlementFailure: loopState.entitlementFailure,
|
|
7154
|
+
allowConfiguredModelFallback: hasConfiguredFallback,
|
|
7155
|
+
});
|
|
7156
|
+
loopState.lastError = nonOkResult.lastError;
|
|
7157
|
+
loopState.authFailureMessage = nonOkResult.authFailureMessage;
|
|
7158
|
+
loopState.sawTransientFailure = nonOkResult.sawTransientFailure;
|
|
7159
|
+
loopState.invalidRequestFailure = nonOkResult.invalidRequestFailure;
|
|
7160
|
+
loopState.entitlementFailure = nonOkResult.entitlementFailure;
|
|
7161
|
+
if (nonOkResult.response !== undefined) {
|
|
7162
|
+
return nonOkResult.response;
|
|
7163
|
+
}
|
|
7164
|
+
if (nonOkResult.continueLoop) {
|
|
7165
|
+
if (nonOkResult.retrySameAccount &&
|
|
7166
|
+
transientSameAccountRetries < MAX_TRANSIENT_SAME_ACCOUNT_RETRIES) {
|
|
7167
|
+
transientSameAccountRetries += 1;
|
|
7168
|
+
const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
|
|
7169
|
+
logger.always(`[proxy] retrying same account=${account.label} after transient upstream ${response.status} (${transientSameAccountRetries}/${MAX_TRANSIENT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
|
|
7170
|
+
await sleep(delayMs);
|
|
7171
|
+
continue;
|
|
7172
|
+
}
|
|
7173
|
+
if (nonOkResult.retrySameAccount) {
|
|
7174
|
+
logger.always(`[proxy] exhausted transient same-account retries for account=${account.label}; rotating`);
|
|
7175
|
+
}
|
|
7176
|
+
if (hasNextAccount && nonOkResult.retryDelayMs) {
|
|
7177
|
+
logger.always(`[proxy] pacing cross-account overload rotation for ${nonOkResult.retryDelayMs}ms`);
|
|
7178
|
+
await sleep(nonOkResult.retryDelayMs);
|
|
7179
|
+
}
|
|
7180
|
+
continue accountLoop;
|
|
7181
|
+
}
|
|
6889
7182
|
break accountLoop;
|
|
6890
7183
|
}
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
7184
|
+
// Clear cooling on success — but only if the stored cooldown has already
|
|
7185
|
+
// expired, so an older in-flight success can't wipe an active exhaustion
|
|
7186
|
+
// cooldown just set by a concurrent 429. The success handler re-applies a
|
|
7187
|
+
// cooldown via reconcileCooldownFromQuota when fresh quota headers
|
|
7188
|
+
// report a rejected window without explicit overage availability.
|
|
7189
|
+
if (accountState.coolingUntil &&
|
|
7190
|
+
Date.now() >= accountState.coolingUntil) {
|
|
7191
|
+
const expiredCooldown = accountState.coolingUntil;
|
|
7192
|
+
accountState.coolingUntil = undefined;
|
|
7193
|
+
accountState.coolingReason = undefined;
|
|
7194
|
+
clearAccountCooldown(account.key, expiredCooldown).catch(() => {
|
|
7195
|
+
// Best-effort cleanup; expired entries are ignored during seeding.
|
|
7196
|
+
});
|
|
7197
|
+
}
|
|
7198
|
+
const successResult = await handleAnthropicSuccessfulResponse({
|
|
6899
7199
|
ctx,
|
|
6900
7200
|
body,
|
|
6901
7201
|
account,
|
|
6902
7202
|
accountState,
|
|
6903
|
-
headers: preparedAttempt.headers,
|
|
6904
|
-
buildUpstreamBody: preparedAttempt.buildUpstreamBody,
|
|
6905
|
-
url,
|
|
6906
|
-
enabledAccounts,
|
|
6907
|
-
orderedAccounts,
|
|
6908
|
-
tracer,
|
|
6909
|
-
requestStartTime,
|
|
6910
|
-
allocateAttemptNumber: () => {
|
|
6911
|
-
loopState.attemptNumber += 1;
|
|
6912
|
-
return loopState.attemptNumber;
|
|
6913
|
-
},
|
|
6914
|
-
upstreamSpan,
|
|
6915
|
-
logAttempt,
|
|
6916
|
-
logProxyBody,
|
|
6917
|
-
logFinalRequest,
|
|
6918
|
-
onStreamTerminal: admissionLease.release,
|
|
6919
|
-
lastError: loopState.lastError,
|
|
6920
|
-
authFailureMessage: loopState.authFailureMessage,
|
|
6921
|
-
entitlementFailure: loopState.entitlementFailure,
|
|
6922
|
-
sawRateLimit: loopState.sawRateLimit,
|
|
6923
|
-
sawTransientFailure: loopState.sawTransientFailure,
|
|
6924
|
-
sawNetworkError: loopState.sawNetworkError,
|
|
6925
|
-
});
|
|
6926
|
-
loopState.lastError = authRetryResult.lastError;
|
|
6927
|
-
loopState.authFailureMessage = authRetryResult.authFailureMessage;
|
|
6928
|
-
loopState.entitlementFailure = authRetryResult.entitlementFailure;
|
|
6929
|
-
loopState.sawRateLimit = authRetryResult.sawRateLimit;
|
|
6930
|
-
loopState.sawTransientFailure = authRetryResult.sawTransientFailure;
|
|
6931
|
-
loopState.sawNetworkError = authRetryResult.sawNetworkError;
|
|
6932
|
-
upstreamSpan = authRetryResult.upstreamSpan;
|
|
6933
|
-
if (authRetryResult.response !== undefined) {
|
|
6934
|
-
admissionTransferredToStream =
|
|
6935
|
-
authRetryResult.holdsAccountAdmission === true;
|
|
6936
|
-
return authRetryResult.response;
|
|
6937
|
-
}
|
|
6938
|
-
if (authRetryResult.continueLoop) {
|
|
6939
|
-
if (hasNextAccount && authRetryResult.retryDelayMs) {
|
|
6940
|
-
logger.always(`[proxy] pacing cross-account SSE overload rotation for ${authRetryResult.retryDelayMs}ms after auth refresh`);
|
|
6941
|
-
await sleep(authRetryResult.retryDelayMs);
|
|
6942
|
-
}
|
|
6943
|
-
continue accountLoop;
|
|
6944
|
-
}
|
|
6945
|
-
}
|
|
6946
|
-
if (!response.ok) {
|
|
6947
|
-
const nonOkResult = await handleAnthropicNonOkResponse({
|
|
6948
7203
|
response,
|
|
6949
|
-
account,
|
|
6950
|
-
accountState,
|
|
6951
|
-
enabledAccounts,
|
|
6952
|
-
orderedAccounts,
|
|
6953
7204
|
tracer,
|
|
6954
7205
|
requestStartTime,
|
|
6955
7206
|
fetchStartMs: preparedAttempt.fetchStartMs,
|
|
6956
7207
|
attemptNumber: loopState.attemptNumber,
|
|
7208
|
+
finalBodyStr: preparedAttempt.finalBodyStr,
|
|
7209
|
+
upstreamSpan,
|
|
6957
7210
|
logAttempt,
|
|
6958
7211
|
logProxyBody,
|
|
6959
7212
|
logFinalRequest,
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
sawTransientFailure: loopState.sawTransientFailure,
|
|
6963
|
-
invalidRequestFailure: loopState.invalidRequestFailure,
|
|
6964
|
-
entitlementFailure: loopState.entitlementFailure,
|
|
6965
|
-
allowConfiguredModelFallback: hasConfiguredFallback,
|
|
7213
|
+
onStreamTerminal: admissionLease.release,
|
|
7214
|
+
poolAccounts: enabledAccounts,
|
|
6966
7215
|
});
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
transientSameAccountRetries < MAX_TRANSIENT_SAME_ACCOUNT_RETRIES) {
|
|
6978
|
-
transientSameAccountRetries += 1;
|
|
6979
|
-
const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
|
|
6980
|
-
logger.always(`[proxy] retrying same account=${account.label} after transient upstream ${response.status} (${transientSameAccountRetries}/${MAX_TRANSIENT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
|
|
6981
|
-
await sleep(delayMs);
|
|
6982
|
-
continue;
|
|
6983
|
-
}
|
|
6984
|
-
if (nonOkResult.retrySameAccount) {
|
|
6985
|
-
logger.always(`[proxy] exhausted transient same-account retries for account=${account.label}; rotating`);
|
|
6986
|
-
}
|
|
6987
|
-
if (hasNextAccount && nonOkResult.retryDelayMs) {
|
|
6988
|
-
logger.always(`[proxy] pacing cross-account overload rotation for ${nonOkResult.retryDelayMs}ms`);
|
|
6989
|
-
await sleep(nonOkResult.retryDelayMs);
|
|
7216
|
+
if ("retryNextAccount" in successResult) {
|
|
7217
|
+
if (successResult.failure) {
|
|
7218
|
+
loopState.lastError = successResult.failure.message;
|
|
7219
|
+
loopState.sawRateLimit ||= successResult.failure.rateLimit;
|
|
7220
|
+
loopState.sawTransientFailure ||=
|
|
7221
|
+
!successResult.failure.rateLimit;
|
|
7222
|
+
if (hasNextAccount && successResult.failure.retryDelayMs) {
|
|
7223
|
+
logger.always(`[proxy] pacing cross-account SSE overload rotation for ${successResult.failure.retryDelayMs}ms`);
|
|
7224
|
+
await sleep(successResult.failure.retryDelayMs);
|
|
7225
|
+
}
|
|
6990
7226
|
}
|
|
6991
7227
|
continue accountLoop;
|
|
6992
7228
|
}
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
// expired, so an older in-flight success can't wipe an active exhaustion
|
|
6997
|
-
// cooldown just set by a concurrent 429. The success handler re-applies a
|
|
6998
|
-
// cooldown via reconcileCooldownFromQuota when fresh quota headers
|
|
6999
|
-
// report a rejected window without explicit overage availability.
|
|
7000
|
-
if (accountState.coolingUntil &&
|
|
7001
|
-
Date.now() >= accountState.coolingUntil) {
|
|
7002
|
-
const expiredCooldown = accountState.coolingUntil;
|
|
7003
|
-
accountState.coolingUntil = undefined;
|
|
7004
|
-
accountState.coolingReason = undefined;
|
|
7005
|
-
clearAccountCooldown(account.key, expiredCooldown).catch(() => {
|
|
7006
|
-
// Best-effort cleanup; expired entries are ignored during seeding.
|
|
7007
|
-
});
|
|
7229
|
+
admissionTransferredToStream =
|
|
7230
|
+
successResult.holdsAccountAdmission === true;
|
|
7231
|
+
return successResult.response;
|
|
7008
7232
|
}
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
account,
|
|
7013
|
-
accountState,
|
|
7014
|
-
response,
|
|
7015
|
-
tracer,
|
|
7016
|
-
requestStartTime,
|
|
7017
|
-
fetchStartMs: preparedAttempt.fetchStartMs,
|
|
7018
|
-
attemptNumber: loopState.attemptNumber,
|
|
7019
|
-
finalBodyStr: preparedAttempt.finalBodyStr,
|
|
7020
|
-
upstreamSpan,
|
|
7021
|
-
logAttempt,
|
|
7022
|
-
logProxyBody,
|
|
7023
|
-
logFinalRequest,
|
|
7024
|
-
onStreamTerminal: admissionLease.release,
|
|
7025
|
-
poolAccounts: enabledAccounts,
|
|
7026
|
-
});
|
|
7027
|
-
if ("retryNextAccount" in successResult) {
|
|
7028
|
-
if (successResult.failure) {
|
|
7029
|
-
loopState.lastError = successResult.failure.message;
|
|
7030
|
-
loopState.sawRateLimit ||= successResult.failure.rateLimit;
|
|
7031
|
-
loopState.sawTransientFailure ||= !successResult.failure.rateLimit;
|
|
7032
|
-
if (hasNextAccount && successResult.failure.retryDelayMs) {
|
|
7033
|
-
logger.always(`[proxy] pacing cross-account SSE overload rotation for ${successResult.failure.retryDelayMs}ms`);
|
|
7034
|
-
await sleep(successResult.failure.retryDelayMs);
|
|
7035
|
-
}
|
|
7233
|
+
finally {
|
|
7234
|
+
if (!admissionTransferredToStream) {
|
|
7235
|
+
admissionLease.release();
|
|
7036
7236
|
}
|
|
7037
|
-
continue accountLoop;
|
|
7038
|
-
}
|
|
7039
|
-
admissionTransferredToStream =
|
|
7040
|
-
successResult.holdsAccountAdmission === true;
|
|
7041
|
-
return successResult.response;
|
|
7042
|
-
}
|
|
7043
|
-
finally {
|
|
7044
|
-
if (!admissionTransferredToStream) {
|
|
7045
|
-
admissionLease.release();
|
|
7046
7237
|
}
|
|
7047
7238
|
}
|
|
7048
7239
|
}
|
|
7049
7240
|
}
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7241
|
+
finally {
|
|
7242
|
+
queuedAccountAdmission?.lease.release();
|
|
7243
|
+
if (loopState.attemptNumber === 0) {
|
|
7244
|
+
acctSelectionSpan?.end();
|
|
7245
|
+
}
|
|
7053
7246
|
}
|
|
7054
7247
|
// Deterministic invalid requests (for example, prompt-too-long) cannot be
|
|
7055
7248
|
// repaired by changing providers. Preserve the authoritative Anthropic 400
|
|
@@ -7098,10 +7291,13 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
7098
7291
|
});
|
|
7099
7292
|
}
|
|
7100
7293
|
fallbackFailureMessage = configuredFallbackResult.lastErrorMessage;
|
|
7294
|
+
let terminalFallbackFailure = configuredFallbackResult.terminalFailure;
|
|
7101
7295
|
// A translation-layer-selected provider is only permitted by an explicit
|
|
7102
7296
|
// routing setting. Empty fallback chains otherwise stay within OAuth.
|
|
7103
7297
|
if (loopState.invalidRequestFailure === null &&
|
|
7104
7298
|
!loopState.sawRateLimit &&
|
|
7299
|
+
!ctx.abortSignal?.aborted &&
|
|
7300
|
+
terminalFallbackFailure?.retryable !== false &&
|
|
7105
7301
|
modelRouter?.isAutoFallbackEnabled?.()) {
|
|
7106
7302
|
const autoFallbackResult = await tryAutoClaudeFallback({
|
|
7107
7303
|
ctx,
|
|
@@ -7116,12 +7312,12 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
7116
7312
|
}
|
|
7117
7313
|
fallbackFailureMessage =
|
|
7118
7314
|
autoFallbackResult.lastErrorMessage ?? fallbackFailureMessage;
|
|
7315
|
+
terminalFallbackFailure =
|
|
7316
|
+
autoFallbackResult.terminalFailure ?? terminalFallbackFailure;
|
|
7119
7317
|
}
|
|
7120
|
-
if (
|
|
7121
|
-
!configuredFallbackResult.invalidRequestFailure) {
|
|
7122
|
-
const failure = configuredFallbackResult.terminalFailure;
|
|
7318
|
+
if (terminalFallbackFailure) {
|
|
7123
7319
|
return buildConfiguredClaudeFallbackFailure({
|
|
7124
|
-
failure,
|
|
7320
|
+
failure: terminalFallbackFailure,
|
|
7125
7321
|
buildLoggedClaudeError,
|
|
7126
7322
|
tracer,
|
|
7127
7323
|
requestStartTime,
|
|
@@ -7178,6 +7374,9 @@ function isClaudeProxyRouteRuntimeOptions(value) {
|
|
|
7178
7374
|
}
|
|
7179
7375
|
function buildEarlyClaudeRequestError(args) {
|
|
7180
7376
|
const { ctx, body, status, message, errorType } = args;
|
|
7377
|
+
if (isProxyRequestFinalized(ctx.requestId)) {
|
|
7378
|
+
return buildClaudeError(status, message);
|
|
7379
|
+
}
|
|
7181
7380
|
recordFinalError(status, PROXY_INTERNAL_ACCOUNT_LABEL, PROXY_INTERNAL_ACCOUNT_TYPE, {
|
|
7182
7381
|
requestId: ctx.requestId,
|
|
7183
7382
|
errorType,
|
|
@@ -7337,7 +7536,7 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
7337
7536
|
if (route.provider === "anthropic") {
|
|
7338
7537
|
tracer?.setMode("passthrough");
|
|
7339
7538
|
if (requestRouting.passthrough) {
|
|
7340
|
-
return handleClaudePassthroughRequest({
|
|
7539
|
+
return await handleClaudePassthroughRequest({
|
|
7341
7540
|
ctx,
|
|
7342
7541
|
body,
|
|
7343
7542
|
clientRequestBody,
|
|
@@ -7347,7 +7546,7 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
7347
7546
|
logFinalRequest,
|
|
7348
7547
|
});
|
|
7349
7548
|
}
|
|
7350
|
-
return handleAnthropicRoutedClaudeRequest({
|
|
7549
|
+
return await handleAnthropicRoutedClaudeRequest({
|
|
7351
7550
|
ctx,
|
|
7352
7551
|
body,
|
|
7353
7552
|
modelRouter: requestModelRouter,
|
|
@@ -7366,7 +7565,7 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
7366
7565
|
});
|
|
7367
7566
|
}
|
|
7368
7567
|
else {
|
|
7369
|
-
return handleTranslatedClaudeRequest({
|
|
7568
|
+
return await handleTranslatedClaudeRequest({
|
|
7370
7569
|
ctx,
|
|
7371
7570
|
body,
|
|
7372
7571
|
route: {
|
|
@@ -7381,10 +7580,43 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
7381
7580
|
}
|
|
7382
7581
|
}
|
|
7383
7582
|
catch (error) {
|
|
7583
|
+
if (ctx.abortSignal?.aborted) {
|
|
7584
|
+
return new Response(JSON.stringify(buildLoggedClaudeError(499, "Client cancelled request", "client_cancelled")), {
|
|
7585
|
+
status: 499,
|
|
7586
|
+
headers: { "content-type": "application/json" },
|
|
7587
|
+
});
|
|
7588
|
+
}
|
|
7589
|
+
const localRefusal = error instanceof ProxyContextPreflightError
|
|
7590
|
+
? error
|
|
7591
|
+
: getProxyTokenBudgetError(error);
|
|
7592
|
+
if (localRefusal) {
|
|
7593
|
+
if (error instanceof ProxyContextPreflightError &&
|
|
7594
|
+
error.evidence) {
|
|
7595
|
+
ctx.metadata.contextPreflight = error.evidence;
|
|
7596
|
+
}
|
|
7597
|
+
const refusal = buildLoggedClaudeError(localRefusal.status, localRefusal.message, localRefusal.code, { errorCode: localRefusal.code, retryable: false });
|
|
7598
|
+
return new Response(JSON.stringify({
|
|
7599
|
+
...refusal,
|
|
7600
|
+
error: {
|
|
7601
|
+
...refusal.error,
|
|
7602
|
+
code: localRefusal.code,
|
|
7603
|
+
retryable: false,
|
|
7604
|
+
},
|
|
7605
|
+
}), {
|
|
7606
|
+
status: localRefusal.status,
|
|
7607
|
+
headers: { "content-type": "application/json" },
|
|
7608
|
+
});
|
|
7609
|
+
}
|
|
7384
7610
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
7385
7611
|
logger.error(`[claude-proxy] Generation error for ${body.model}: ${errMsg}`);
|
|
7386
7612
|
tracer?.setError("generation_error", errMsg.slice(0, 500));
|
|
7387
7613
|
tracer?.end(502, Date.now() - requestStartTime);
|
|
7614
|
+
// Native transport errors retain their original code/cause and
|
|
7615
|
+
// last-attempt account through the runtime error boundary. Only
|
|
7616
|
+
// explicit local admission refusals are converted above.
|
|
7617
|
+
if (route.provider === "anthropic") {
|
|
7618
|
+
throw error;
|
|
7619
|
+
}
|
|
7388
7620
|
return buildLoggedClaudeError(502, `Generation failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
7389
7621
|
}
|
|
7390
7622
|
},
|