@juspay/neurolink 12.17.1 → 12.17.2

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/browser/neurolink.min.js +421 -412
  3. package/dist/cli/commands/proxy.js +455 -259
  4. package/dist/constants/contextWindows.d.ts +2 -0
  5. package/dist/constants/contextWindows.js +4 -0
  6. package/dist/proxy/anthropicBudgetResponse.d.ts +3 -0
  7. package/dist/proxy/anthropicBudgetResponse.js +153 -0
  8. package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
  9. package/dist/proxy/bodyCaptureProcessing.js +23 -4
  10. package/dist/proxy/bodyCaptureWorker.js +20 -2
  11. package/dist/proxy/codexFallback.d.ts +10 -0
  12. package/dist/proxy/codexFallback.js +108 -24
  13. package/dist/proxy/codexUsage.js +2 -0
  14. package/dist/proxy/globalInstaller.d.ts +32 -1
  15. package/dist/proxy/globalInstaller.js +386 -3
  16. package/dist/proxy/otelLogSink.d.ts +3 -3
  17. package/dist/proxy/otelLogSink.js +137 -144
  18. package/dist/proxy/otlpLogTransport.d.ts +3 -0
  19. package/dist/proxy/otlpLogTransport.js +334 -0
  20. package/dist/proxy/proxyActivity.d.ts +22 -2
  21. package/dist/proxy/proxyActivity.js +114 -9
  22. package/dist/proxy/proxyAnalysis.js +82 -52
  23. package/dist/proxy/proxyClientCancellation.d.ts +7 -0
  24. package/dist/proxy/proxyClientCancellation.js +23 -0
  25. package/dist/proxy/proxyContextPreflight.d.ts +25 -0
  26. package/dist/proxy/proxyContextPreflight.js +337 -0
  27. package/dist/proxy/proxyFailureDetails.d.ts +19 -0
  28. package/dist/proxy/proxyFailureDetails.js +96 -0
  29. package/dist/proxy/proxyLifecycle.js +7 -0
  30. package/dist/proxy/proxyRouteBodyCapture.d.ts +13 -0
  31. package/dist/proxy/proxyRouteBodyCapture.js +112 -0
  32. package/dist/proxy/proxyTokenBudget.d.ts +40 -0
  33. package/dist/proxy/proxyTokenBudget.js +523 -0
  34. package/dist/proxy/proxyTracer.d.ts +3 -2
  35. package/dist/proxy/proxyTracer.js +46 -20
  36. package/dist/proxy/proxyTranslationEngine.d.ts +8 -10
  37. package/dist/proxy/proxyTranslationEngine.js +736 -376
  38. package/dist/proxy/requestLogger.d.ts +1 -0
  39. package/dist/proxy/requestLogger.js +107 -5
  40. package/dist/proxy/restartControl.js +9 -4
  41. package/dist/proxy/rollingWorkerProcess.js +65 -0
  42. package/dist/proxy/rollingWorkerSupervisor.js +7 -0
  43. package/dist/proxy/updateCoordinator.d.ts +14 -1
  44. package/dist/proxy/updateCoordinator.js +57 -2
  45. package/dist/server/routes/claudeProxyRoutes.d.ts +4 -5
  46. package/dist/server/routes/claudeProxyRoutes.js +809 -577
  47. package/dist/server/routes/codexProxyRoutes.js +191 -9
  48. package/dist/server/routes/openaiProxyRoutes.d.ts +1 -1
  49. package/dist/server/routes/openaiProxyRoutes.js +261 -69
  50. package/dist/types/cli.d.ts +3 -0
  51. package/dist/types/codex.d.ts +5 -1
  52. package/dist/types/index.d.ts +2 -0
  53. package/dist/types/index.js +2 -0
  54. package/dist/types/proxy.d.ts +148 -3
  55. package/dist/types/proxy.js +0 -15
  56. package/dist/types/proxyBudget.d.ts +74 -0
  57. package/dist/types/proxyBudget.js +1 -0
  58. package/dist/types/proxyContext.d.ts +35 -0
  59. package/dist/types/proxyContext.js +1 -0
  60. package/docs/assets/dashboards/README.md +37 -0
  61. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +197 -89
  62. package/docs-site/static/search-index.json +1 -1
  63. package/package.json +3 -1
  64. package/scripts/observability/NATIVE-DURABLE.md +63 -0
  65. package/scripts/observability/otel-collector.proxy-native-durable.yaml +93 -0
  66. package/scripts/observability/proxy-telemetry-backend.mjs +32 -5
  67. package/scripts/observability/proxy-telemetry-check.mjs +396 -61
  68. 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, generateToolUseId, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
29
- import { CodexFallbackResponseError, consumeCodexFallbackResponse, createCodexFallbackStream, convertClaudeRequestToCodex, } from "../../proxy/codexFallback.js";
30
- import { registerProxyResponseObserver, takeProxyResponseObservers, trackProxyResponse, } from "../../proxy/proxyActivity.js";
31
- import { buildAnthropicModelsListResponse, buildTranslationOptions, extractText, extractToolArgs, extractUsageFromStreamResult, handleTranslatedJsonRequest, handleTranslatedStreamRequest, hasTranslatedOutput, } from "../../proxy/proxyTranslationEngine.js";
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
- return fetch(url, {
166
- ...init,
167
- dispatcher: anthropicUpstreamDispatcher,
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.timeout(UPSTREAM_FETCH_TIMEOUT_MS),
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
- async function executeClaudeFallbackTranslation(args) {
3509
- const { ctx, body, tracer, requestStartTime, logProxyBody, logFinalRequest, options, providerLabel, idleTimeoutMs = FALLBACK_STREAM_IDLE_TIMEOUT_MS, } = args;
3510
- const fallbackAbortController = new AbortController();
3511
- let streamResult;
3512
- try {
3513
- streamResult = await withTimeout(ctx.neurolink.stream({
3514
- ...options,
3515
- abortSignal: fallbackAbortController.signal,
3516
- }), idleTimeoutMs, `Fallback ${providerLabel} initialization timed out after ${idleTimeoutMs}ms`);
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
- const consumeFallbackStream = async (onText) => {
3523
- const iterator = streamResult.stream[Symbol.asyncIterator]();
3524
- let collectedText = "";
3525
- try {
3526
- while (true) {
3527
- const { value: chunk, done } = await withTimeout(iterator.next(), idleTimeoutMs, `Fallback ${providerLabel} stream timed out after ${idleTimeoutMs}ms of inactivity`);
3528
- if (done) {
3529
- return collectedText;
3530
- }
3531
- const text = extractText(chunk);
3532
- if (text) {
3533
- collectedText += text;
3534
- onText?.(text);
3535
- }
3536
- }
3537
- }
3538
- catch (error) {
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
- const serializer = new ClaudeStreamSerializer(body.model, 0);
3548
- // Eagerly consume stream so errors fire synchronously and the
3549
- // fallback loop in tryConfiguredClaudeFallbackChain can catch them.
3550
- const frames = [];
3551
- for (const frame of serializer.start()) {
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
- const internal = {
3609
- content: collectedText,
3610
- model: streamResult.model,
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 (!isRetryableNetworkError(error) ||
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
- const finish = (status, result, errorType, message) => {
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?.input,
3738
- outputTokens: result?.usage?.output,
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 serializer = new ClaudeStreamSerializer(body.model);
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
- ...serializer.emitError(502, "Codex fallback stream failed"),
3843
- ].map(capture);
3844
- finish(502, undefined, "stream_error", detail);
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?.input,
3892
- outputTokens: parsed.usage?.output,
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
- const status = ctx.abortSignal?.aborted
4084
- ? 499
4085
- : fallbackErr instanceof CodexFallbackResponseError
4086
- ? fallbackErr.status
4087
- : 502;
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: `Configured fallback ${fallback.provider}/${fallback.model} failed (HTTP ${status})`,
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 === 429
4094
- ? "rate_limit_error"
4095
- : status === 401
4096
- ? "authentication_error"
4097
- : status === 403
4098
- ? "permission_error"
4099
- : status === 400
4100
- ? "invalid_request_error"
4101
- : "api_error",
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
- const body = buildLoggedClaudeError(failure.status, failure.message, failure.errorType);
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
- return { response: null, lastErrorMessage: errorMessage };
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 retryBodyStr = buildUpstreamBody(account.token).bodyStr;
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: retryBodyStr,
5285
- signal: AbortSignal.timeout(UPSTREAM_FETCH_TIMEOUT_MS),
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: retryBodyStr,
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: retryBodyStr,
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 = resolveRequestLogAccountIdentity(finalAccountLabel, finalAccountType);
6010
- if (status >= 400) {
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) => isOAuth
6280
- ? polyfillOAuthBody(bodyStr, token, snapshot, isClaudeClientRequest, headers["x-claude-code-session-id"])
6281
- : { bodyStr };
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.timeout(UPSTREAM_FETCH_TIMEOUT_MS),
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
- if (accountAdmissionCapacity !== undefined &&
6661
- effectiveAccounts.length > 0 &&
6662
- effectiveAccounts.every((account) => !isAccountAdmissionAvailable(account.key, accountAdmissionCapacity))) {
6663
- queuedAccountAdmission = await acquireFirstAvailableAccountAdmission(effectiveAccounts.map((account) => account.key), accountAdmissionCapacity, ctx.abortSignal);
6664
- }
6665
- accountLoop: for (const [accountIndex, account,] of effectiveAccounts.entries()) {
6666
- const hasNextAccount = accountIndex < effectiveAccounts.length - 1;
6667
- const accountState = getOrCreateRuntimeState(account.key);
6668
- let transientSameAccountRetries = 0;
6669
- let rateLimitSameAccountRetries = 0;
6670
- while (true) {
6671
- const transportPermit = await providerTransportCoordinator.acquire(ctx.abortSignal);
6672
- if (transportPermit.allowed === false) {
6673
- loopState.sawNetworkError = true;
6674
- loopState.lastTransportErrorCode =
6675
- transportPermit.errorCode ?? undefined;
6676
- loopState.lastTransportScope = transportPermit.transportScope;
6677
- loopState.lastError = `Anthropic transport recovery probe failed (${transportPermit.errorCode ?? "unknown"})`;
6678
- // The probe that failed was another request's attempt; this one has
6679
- // sent nothing. Give it the same bounded same-account budget a direct
6680
- // transport failure gets, instead of failing every queued request the
6681
- // moment one probe times out.
6682
- const probeRetryBudget = transportPermit.connectPhase
6683
- ? MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES
6684
- : MAX_TRANSIENT_SAME_ACCOUNT_RETRIES;
6685
- if (transientSameAccountRetries < probeRetryBudget) {
6686
- transientSameAccountRetries += 1;
6687
- const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
6688
- logger.always(`[proxy] retrying same account=${account.label} after failed transport recovery probe (${transientSameAccountRetries}/${probeRetryBudget}) in ${delayMs}ms`);
6689
- await sleep(delayMs);
6690
- continue;
6691
- }
6692
- break accountLoop;
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
- continue accountLoop;
6761
- }
6762
- let admissionTransferredToStream = false;
6763
- try {
6764
- let fetchResult;
6765
- try {
6766
- fetchResult = await fetchAnthropicAccountResponse({
6767
- url,
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
- catch (error) {
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
- throw error;
6925
+ continue accountLoop;
6791
6926
  }
6792
- if (fetchResult.transportScope) {
6793
- providerTransportCoordinator.reportTransportFailure(fetchResult.errorCode, fetchResult.transportScope, transportPermit, fetchResult.connectPhase === true);
6927
+ let admissionLease;
6928
+ if (queuedAccountAdmission?.accountKey === account.key) {
6929
+ admissionLease = queuedAccountAdmission.lease;
6930
+ queuedAccountAdmission = undefined;
6794
6931
  }
6795
6932
  else {
6796
- providerTransportCoordinator.reportSuccess(transportPermit);
6797
- }
6798
- loopState.lastError = fetchResult.lastError;
6799
- loopState.sawRateLimit = fetchResult.sawRateLimit;
6800
- loopState.sawNetworkError = fetchResult.sawNetworkError;
6801
- if (fetchResult.transportScope) {
6802
- loopState.lastTransportScope = fetchResult.transportScope;
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 (fetchResult.terminalError) {
6806
- return finalizeAnthropicTerminalFetchError({
6807
- terminalError: fetchResult.terminalError,
6808
- account,
6809
- tracer,
6810
- requestStartTime,
6811
- attemptNumber: loopState.attemptNumber,
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
- if (fetchResult.continueLoop || !fetchResult.response) {
6817
- // Genuine 429 (carries a cooldown plan derived from quota headers).
6818
- if (fetchResult.cooldownPlan) {
6819
- const plan = fetchResult.cooldownPlan;
6820
- // Refresh the account's quota snapshot for proactive selection.
6821
- if (fetchResult.quota) {
6822
- accountState.quota = mergeQuotaSnapshot(accountState.quota, fetchResult.quota);
6823
- saveAccountQuota(account.key, fetchResult.quota).catch(() => {
6824
- // Non-fatal: routing already has the in-memory snapshot.
6825
- });
6826
- }
6827
- // Publish the cooldown before retrying so requests arriving behind
6828
- // this one skip the throttled account instead of joining the burst.
6829
- let cooldownExtended = false;
6830
- if (plan.scope === "account" &&
6831
- (!accountState.coolingUntil ||
6832
- plan.coolingUntil > accountState.coolingUntil)) {
6833
- accountState.coolingUntil = plan.coolingUntil;
6834
- accountState.coolingReason = plan.reason;
6835
- cooldownExtended = true;
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
- if (cooldownExtended && plan.scope === "account") {
6838
- await saveAccountCooldown(account.key, accountState.coolingUntil ?? plan.coolingUntil, accountState.coolingReason ?? plan.reason).catch(() => {
6839
- // Non-fatal: routing already has the in-memory cooldown.
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 retries are budgeted across all concurrent requests for
6843
- // this account/window. Exhaustion plans rotate immediately and never
6844
- // claim this budget.
6845
- const sharedRetrySlot = fetchResult.retrySameAccount
6846
- ? claimTransientRateLimitRetry(account.key, plan.coolingUntil)
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
- fetchResult.retryAfterMs !== undefined &&
6850
- rateLimitSameAccountRetries <
6851
- MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES &&
6852
- sharedRetrySlot !== undefined) {
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
- // Exhaustion, or the shared transient retry budget being used up:
6863
- // rotate while the already-published cooldown remains active.
6864
- advancePrimaryIfCurrent(account.key, enabledAccounts.length, orderedAccounts[0]?.key);
6865
- logger.always(`[proxy] account=${account.label} rate-limited (${plan.reason}); cooling ~${minutesUntil(plan.coolingUntil, Date.now())}m until ${new Date(plan.coolingUntil).toISOString()}, rotating`);
6866
- if (plan.rotateImmediately) {
6867
- scheduleHandoffQuotaRefresh(account, effectiveAccounts[accountIndex + 1], plan.coolingUntil, sessionSoftLimit);
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
- // Transient error retry (network errors, 529 overloaded). A failure
6872
- // from the connect phase sent nothing, so it earns the larger budget.
6873
- const sameAccountRetryBudget = fetchResult.connectPhase
6874
- ? MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES
6875
- : MAX_TRANSIENT_SAME_ACCOUNT_RETRIES;
6876
- if (fetchResult.retrySameAccount &&
6877
- transientSameAccountRetries < sameAccountRetryBudget) {
6878
- transientSameAccountRetries += 1;
6879
- const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
6880
- logger.always(`[proxy] retrying same account=${account.label} after transient network error (${transientSameAccountRetries}/${sameAccountRetryBudget}) in ${delayMs}ms`);
6881
- await sleep(delayMs);
6882
- continue;
6883
- }
6884
- if (fetchResult.retrySameAccount && !fetchResult.transportScope) {
6885
- logger.always(`[proxy] exhausted transient same-account retries for account=${account.label}; rotating`);
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 (fetchResult.transportScope) {
6888
- logger.always(`[proxy] Anthropic ${fetchResult.transportScope} failure code=${fetchResult.errorCode ?? "unknown"}; suppressing cross-account rotation after ${transientSameAccountRetries + 1} attempts`);
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
- continue accountLoop;
6892
- }
6893
- let upstreamSpan = fetchResult.upstreamSpan;
6894
- const response = fetchResult.response;
6895
- if (response.status === 401 &&
6896
- account.type === "oauth" &&
6897
- account.refreshToken) {
6898
- const authRetryResult = await handleAnthropicAuthRetry({
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
- lastError: loopState.lastError,
6961
- authFailureMessage: loopState.authFailureMessage,
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
- loopState.lastError = nonOkResult.lastError;
6968
- loopState.authFailureMessage = nonOkResult.authFailureMessage;
6969
- loopState.sawTransientFailure = nonOkResult.sawTransientFailure;
6970
- loopState.invalidRequestFailure = nonOkResult.invalidRequestFailure;
6971
- loopState.entitlementFailure = nonOkResult.entitlementFailure;
6972
- if (nonOkResult.response !== undefined) {
6973
- return nonOkResult.response;
6974
- }
6975
- if (nonOkResult.continueLoop) {
6976
- if (nonOkResult.retrySameAccount &&
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
- break accountLoop;
6994
- }
6995
- // Clear cooling on success — but only if the stored cooldown has already
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
- const successResult = await handleAnthropicSuccessfulResponse({
7010
- ctx,
7011
- body,
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
- queuedAccountAdmission?.lease.release();
7051
- if (loopState.attemptNumber === 0) {
7052
- acctSelectionSpan?.end();
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 (configuredFallbackResult.terminalFailure &&
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
  },