@juspay/neurolink 10.4.2 → 10.4.4

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 (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/auth/tokenStore.d.ts +7 -0
  3. package/dist/auth/tokenStore.js +54 -0
  4. package/dist/browser/neurolink.min.js +377 -380
  5. package/dist/cli/commands/proxy.d.ts +2 -1
  6. package/dist/cli/commands/proxy.js +290 -63
  7. package/dist/cli/commands/proxyAnalyze.js +10 -1
  8. package/dist/lib/auth/tokenStore.d.ts +7 -0
  9. package/dist/lib/auth/tokenStore.js +54 -0
  10. package/dist/lib/proxy/proxyAnalysis.js +136 -12
  11. package/dist/lib/proxy/proxyTranslationEngine.d.ts +1 -0
  12. package/dist/lib/proxy/proxyTranslationEngine.js +56 -12
  13. package/dist/lib/proxy/rawStreamCapture.js +47 -1
  14. package/dist/lib/proxy/requestLogger.d.ts +2 -0
  15. package/dist/lib/proxy/requestLogger.js +72 -13
  16. package/dist/lib/proxy/rollingProxyServer.js +79 -8
  17. package/dist/lib/proxy/rollingWorkerProcess.js +20 -15
  18. package/dist/lib/proxy/rollingWorkerProtocol.js +3 -0
  19. package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
  20. package/dist/lib/proxy/rollingWorkerSupervisor.js +31 -11
  21. package/dist/lib/proxy/runtimeConfig.d.ts +1 -0
  22. package/dist/lib/proxy/runtimeConfig.js +20 -5
  23. package/dist/lib/proxy/socketWorkerRuntime.js +41 -13
  24. package/dist/lib/proxy/sseInterceptor.js +47 -1
  25. package/dist/lib/proxy/tokenRefresh.d.ts +6 -0
  26. package/dist/lib/proxy/tokenRefresh.js +70 -15
  27. package/dist/lib/proxy/usageStats.d.ts +25 -3
  28. package/dist/lib/proxy/usageStats.js +546 -55
  29. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -0
  30. package/dist/lib/server/routes/claudeProxyRoutes.js +261 -297
  31. package/dist/lib/types/proxy.d.ts +90 -1
  32. package/dist/proxy/proxyAnalysis.js +136 -12
  33. package/dist/proxy/proxyTranslationEngine.d.ts +1 -0
  34. package/dist/proxy/proxyTranslationEngine.js +56 -12
  35. package/dist/proxy/rawStreamCapture.js +47 -1
  36. package/dist/proxy/requestLogger.d.ts +2 -0
  37. package/dist/proxy/requestLogger.js +72 -13
  38. package/dist/proxy/rollingProxyServer.js +79 -8
  39. package/dist/proxy/rollingWorkerProcess.js +20 -15
  40. package/dist/proxy/rollingWorkerProtocol.js +3 -0
  41. package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
  42. package/dist/proxy/rollingWorkerSupervisor.js +31 -11
  43. package/dist/proxy/runtimeConfig.d.ts +1 -0
  44. package/dist/proxy/runtimeConfig.js +20 -5
  45. package/dist/proxy/socketWorkerRuntime.js +41 -13
  46. package/dist/proxy/sseInterceptor.js +47 -1
  47. package/dist/proxy/tokenRefresh.d.ts +6 -0
  48. package/dist/proxy/tokenRefresh.js +70 -15
  49. package/dist/proxy/usageStats.d.ts +25 -3
  50. package/dist/proxy/usageStats.js +546 -55
  51. package/dist/server/routes/claudeProxyRoutes.d.ts +2 -0
  52. package/dist/server/routes/claudeProxyRoutes.js +261 -297
  53. package/dist/types/proxy.d.ts +90 -1
  54. package/package.json +1 -1
@@ -26,7 +26,7 @@ import { relocateClientSystemIntoMessages } from "../../proxy/systemRelocation.j
26
26
  import { logBodyCapture, logRequest, logRequestAttempt, } from "../../proxy/requestLogger.js";
27
27
  import { createSSEInterceptor } from "../../proxy/sseInterceptor.js";
28
28
  import { createStreamTerminalOutcomeTracker, mergeStreamTerminalOutcome, preflightAnthropicStream, } from "../../proxy/streamOutcome.js";
29
- import { isPermanentRefreshFailure, needsRefresh, persistTokens, refreshToken, } from "../../proxy/tokenRefresh.js";
29
+ import { isPermanentRefreshFailure, needsRefresh, persistTokens, refreshToken, refreshTokenFromLatest, } from "../../proxy/tokenRefresh.js";
30
30
  import { buildProxyTranslationPlan, parseRetryAfterMs, } from "../../proxy/routingPolicy.js";
31
31
  import { writeJsonSnapshotAtomically } from "../../proxy/snapshotPersistence.js";
32
32
  import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "../../proxy/usageStats.js";
@@ -45,6 +45,8 @@ const BLOCKED_UPSTREAM_HEADERS = new Set([
45
45
  "content-length",
46
46
  "transfer-encoding",
47
47
  ]);
48
+ const PROXY_INTERNAL_ACCOUNT_LABEL = "proxy/internal";
49
+ const PROXY_INTERNAL_ACCOUNT_TYPE = "internal";
48
50
  // ---------------------------------------------------------------------------
49
51
  // Module-level state
50
52
  // ---------------------------------------------------------------------------
@@ -959,15 +961,36 @@ async function handleTranslatedClaudeRequest(args) {
959
961
  requestStartTime,
960
962
  });
961
963
  }
962
- return handleTranslatedJsonRequest({
963
- ctx,
964
- format: "claude",
965
- requestModel: body.model,
966
- parsed,
967
- attempts,
968
- tracer,
969
- requestStartTime,
970
- });
964
+ try {
965
+ return await handleTranslatedJsonRequest({
966
+ ctx,
967
+ format: "claude",
968
+ requestModel: body.model,
969
+ parsed,
970
+ attempts,
971
+ tracer,
972
+ requestStartTime,
973
+ terminalFailureStatus: 502,
974
+ });
975
+ }
976
+ catch (error) {
977
+ const message = error instanceof Error ? error.message : "unknown error";
978
+ logger.error(`[claude-proxy] Translated generation failed for ${body.model}: ${message}`);
979
+ const clientError = buildClaudeError(502, `Generation failed: ${message}`);
980
+ const clientErrorBody = JSON.stringify(clientError);
981
+ logProxyBody({
982
+ phase: "client_response",
983
+ headers: { "content-type": "application/json" },
984
+ body: clientErrorBody,
985
+ bodySize: Buffer.byteLength(clientErrorBody, "utf8"),
986
+ contentType: "application/json",
987
+ account: "translation",
988
+ accountType: "translation",
989
+ responseStatus: 502,
990
+ durationMs: Date.now() - requestStartTime,
991
+ });
992
+ return clientError;
993
+ }
971
994
  }
972
995
  function logProxyRoutingPlan(logProxyBody, stage, plan) {
973
996
  logProxyBody({
@@ -980,7 +1003,7 @@ function logProxyRoutingPlan(logProxyBody, stage, plan) {
980
1003
  });
981
1004
  }
982
1005
  async function handleClaudePassthroughRequest(args) {
983
- const { ctx, body, clientRequestBody, tracer, requestStartTime, logProxyBody, } = args;
1006
+ const { ctx, body, clientRequestBody, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
984
1007
  tracer?.setMode("passthrough-cli");
985
1008
  const bodyStr = clientRequestBody;
986
1009
  const toolCount = Array.isArray(body.tools) ? body.tools.length : 0;
@@ -1016,6 +1039,7 @@ async function handleClaudePassthroughRequest(args) {
1016
1039
  upstreamUrl: "https://api.anthropic.com/v1/messages?beta=true",
1017
1040
  },
1018
1041
  });
1042
+ recordAttempt("passthrough", "passthrough");
1019
1043
  let response;
1020
1044
  try {
1021
1045
  response = await fetch("https://api.anthropic.com/v1/messages?beta=true", {
@@ -1027,24 +1051,11 @@ async function handleClaudePassthroughRequest(args) {
1027
1051
  }
1028
1052
  catch (fetchErr) {
1029
1053
  const errMsg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
1054
+ recordAttemptError("passthrough", "passthrough", 502);
1030
1055
  tracer?.setError("network_error", errMsg);
1031
1056
  upstreamSpan?.end();
1032
1057
  tracer?.end(502, Date.now() - requestStartTime);
1033
- logRequest({
1034
- timestamp: new Date().toISOString(),
1035
- requestId: ctx.requestId,
1036
- method: ctx.method,
1037
- path: ctx.path,
1038
- model: body.model,
1039
- stream: body.stream ?? false,
1040
- toolCount,
1041
- account: "passthrough",
1042
- accountType: "passthrough",
1043
- responseStatus: 502,
1044
- responseTimeMs: Date.now() - requestStartTime,
1045
- errorType: "network_error",
1046
- errorMessage: errMsg,
1047
- });
1058
+ logFinalRequest(502, "passthrough", "passthrough", "network_error", errMsg);
1048
1059
  const errorBody = buildClaudeError(502, `Passthrough fetch failed: ${errMsg}`);
1049
1060
  const errorBodyText = JSON.stringify(errorBody);
1050
1061
  logProxyBody({
@@ -1068,6 +1079,7 @@ async function handleClaudePassthroughRequest(args) {
1068
1079
  tracer?.logUpstreamResponseHeaders(upstreamResponseHeaders);
1069
1080
  if (!response.ok) {
1070
1081
  const errorText = await response.text();
1082
+ recordAttemptError("passthrough", "passthrough", response.status, response.status === 429 ? "quota" : undefined);
1071
1083
  tracer?.logUpstreamResponseBody(errorText);
1072
1084
  logProxyBody({
1073
1085
  phase: "upstream_response",
@@ -1096,6 +1108,7 @@ async function handleClaudePassthroughRequest(args) {
1096
1108
  upstreamSpan?.end();
1097
1109
  tracer?.setError("api_error", errorText.slice(0, 500));
1098
1110
  tracer?.end(response.status, Date.now() - requestStartTime);
1111
+ logFinalRequest(response.status, "passthrough", "passthrough", response.status === 429 ? "rate_limit_error" : "api_error", errorText);
1099
1112
  try {
1100
1113
  return JSON.parse(errorText);
1101
1114
  }
@@ -1115,6 +1128,7 @@ async function handleClaudePassthroughRequest(args) {
1115
1128
  upstreamSpan,
1116
1129
  upstreamResponseHeaders,
1117
1130
  logProxyBody,
1131
+ logFinalRequest,
1118
1132
  });
1119
1133
  }
1120
1134
  return handleClaudePassthroughJsonResponse({
@@ -1128,6 +1142,7 @@ async function handleClaudePassthroughRequest(args) {
1128
1142
  upstreamSpan,
1129
1143
  upstreamResponseHeaders,
1130
1144
  logProxyBody,
1145
+ logFinalRequest,
1131
1146
  });
1132
1147
  }
1133
1148
  function trackUpstreamReadableStream(source) {
@@ -1168,15 +1183,28 @@ function trackUpstreamReadableStream(source) {
1168
1183
  return { stream, outcome: tracker.outcome };
1169
1184
  }
1170
1185
  async function handleClaudePassthroughStreamResponse(args) {
1171
- const { ctx, body, bodyStr, response, tracer, requestStartTime, toolCount, upstreamSpan, upstreamResponseHeaders, logProxyBody, } = args;
1186
+ const { bodyStr, response, tracer, requestStartTime, upstreamSpan, upstreamResponseHeaders, logProxyBody, logFinalRequest, } = args;
1172
1187
  const responseHeaders = { ...upstreamResponseHeaders };
1173
1188
  const { stream: clientCaptureStream, capture: clientCapture } = createRawStreamCapture();
1174
1189
  const responseBody = response.body;
1175
1190
  if (!responseBody) {
1191
+ recordAttemptError("passthrough", "passthrough", 502);
1176
1192
  throw new Error("Expected passthrough stream response body");
1177
1193
  }
1178
1194
  const trackedStream = trackUpstreamReadableStream(responseBody);
1179
1195
  let streamSource = trackedStream.stream;
1196
+ let streamFinalized = false;
1197
+ const finalizeStream = (status, errorType, errorMessage, usage) => {
1198
+ if (streamFinalized) {
1199
+ return false;
1200
+ }
1201
+ streamFinalized = true;
1202
+ if (errorType === "stream_error") {
1203
+ recordAttemptError("passthrough", "passthrough", status);
1204
+ }
1205
+ logFinalRequest(status, "passthrough", "passthrough", errorType, errorMessage, usage);
1206
+ return true;
1207
+ };
1180
1208
  if (tracer) {
1181
1209
  try {
1182
1210
  const { stream: interceptor, telemetry } = createSSEInterceptor({
@@ -1224,31 +1252,11 @@ async function handleClaudePassthroughStreamResponse(args) {
1224
1252
  capturedTracer.setError(failure.errorType, failure.message);
1225
1253
  }
1226
1254
  capturedTracer.end(failure?.status ?? response.status, Date.now() - requestStartTime);
1227
- const traceCtx = capturedTracer.getTraceContext();
1228
- logRequest({
1229
- timestamp: new Date().toISOString(),
1230
- requestId: ctx.requestId,
1231
- method: ctx.method,
1232
- path: ctx.path,
1233
- model: body.model,
1234
- stream: true,
1235
- toolCount,
1236
- account: "passthrough",
1237
- accountType: "passthrough",
1238
- responseStatus: failure?.status ?? response.status,
1239
- responseTimeMs: Date.now() - requestStartTime,
1255
+ finalizeStream(failure?.status ?? response.status, failure?.errorType, failure?.message, {
1240
1256
  inputTokens: data.usage.inputTokens,
1241
1257
  outputTokens: data.usage.outputTokens,
1242
1258
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
1243
1259
  cacheReadTokens: data.usage.cacheReadInputTokens,
1244
- traceId: traceCtx.traceId,
1245
- spanId: traceCtx.spanId,
1246
- ...(failure
1247
- ? {
1248
- errorType: failure.errorType,
1249
- errorMessage: failure.message,
1250
- }
1251
- : {}),
1252
1260
  });
1253
1261
  logProxyBody({
1254
1262
  phase: "upstream_response",
@@ -1276,27 +1284,14 @@ async function handleClaudePassthroughStreamResponse(args) {
1276
1284
  });
1277
1285
  })
1278
1286
  .catch((error) => {
1279
- capturedTracer.setError("stream_error", error instanceof Error ? error.message : String(error));
1287
+ if (streamFinalized) {
1288
+ return;
1289
+ }
1290
+ const message = error instanceof Error ? error.message : String(error);
1291
+ capturedTracer.setError("stream_telemetry_error", message);
1280
1292
  capturedUpstreamSpan?.end();
1281
1293
  capturedTracer.end(500, Date.now() - requestStartTime);
1282
- const traceCtx = capturedTracer.getTraceContext();
1283
- logRequest({
1284
- timestamp: new Date().toISOString(),
1285
- requestId: ctx.requestId,
1286
- method: ctx.method,
1287
- path: ctx.path,
1288
- model: body.model,
1289
- stream: true,
1290
- toolCount,
1291
- account: "passthrough",
1292
- accountType: "passthrough",
1293
- responseStatus: 500,
1294
- responseTimeMs: Date.now() - requestStartTime,
1295
- errorType: "stream_error",
1296
- errorMessage: error instanceof Error ? error.message : String(error),
1297
- traceId: traceCtx.traceId,
1298
- spanId: traceCtx.spanId,
1299
- });
1294
+ finalizeStream(500, "stream_telemetry_error", message);
1300
1295
  });
1301
1296
  }
1302
1297
  catch {
@@ -1307,28 +1302,7 @@ async function handleClaudePassthroughStreamResponse(args) {
1307
1302
  tracer.setError(failure.errorType, failure.message);
1308
1303
  }
1309
1304
  tracer.end(failure?.status ?? response.status, Date.now() - requestStartTime);
1310
- const traceCtx = tracer.getTraceContext();
1311
- logRequest({
1312
- timestamp: new Date().toISOString(),
1313
- requestId: ctx.requestId,
1314
- method: ctx.method,
1315
- path: ctx.path,
1316
- model: body.model,
1317
- stream: true,
1318
- toolCount,
1319
- account: "passthrough",
1320
- accountType: "passthrough",
1321
- responseStatus: failure?.status ?? response.status,
1322
- responseTimeMs: Date.now() - requestStartTime,
1323
- traceId: traceCtx.traceId,
1324
- spanId: traceCtx.spanId,
1325
- ...(failure
1326
- ? {
1327
- errorType: failure.errorType,
1328
- errorMessage: failure.message,
1329
- }
1330
- : {}),
1331
- });
1305
+ finalizeStream(failure?.status ?? response.status, failure?.errorType, failure?.message);
1332
1306
  });
1333
1307
  }
1334
1308
  }
@@ -1342,28 +1316,11 @@ async function handleClaudePassthroughStreamResponse(args) {
1342
1316
  .then(([data, clientBody, rawOutcome]) => {
1343
1317
  const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
1344
1318
  const failure = getStreamFailureDetails(terminalOutcome);
1345
- logRequest({
1346
- timestamp: new Date().toISOString(),
1347
- requestId: ctx.requestId,
1348
- method: ctx.method,
1349
- path: ctx.path,
1350
- model: body.model,
1351
- stream: true,
1352
- toolCount,
1353
- account: "passthrough",
1354
- accountType: "passthrough",
1355
- responseStatus: failure?.status ?? response.status,
1356
- responseTimeMs: Date.now() - requestStartTime,
1319
+ finalizeStream(failure?.status ?? response.status, failure?.errorType, failure?.message, {
1357
1320
  inputTokens: data.usage.inputTokens,
1358
1321
  outputTokens: data.usage.outputTokens,
1359
1322
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
1360
1323
  cacheReadTokens: data.usage.cacheReadInputTokens,
1361
- ...(failure
1362
- ? {
1363
- errorType: failure.errorType,
1364
- errorMessage: failure.message,
1365
- }
1366
- : {}),
1367
1324
  });
1368
1325
  logProxyBody({
1369
1326
  phase: "upstream_response",
@@ -1391,21 +1348,9 @@ async function handleClaudePassthroughStreamResponse(args) {
1391
1348
  });
1392
1349
  })
1393
1350
  .catch((error) => {
1394
- logRequest({
1395
- timestamp: new Date().toISOString(),
1396
- requestId: ctx.requestId,
1397
- method: ctx.method,
1398
- path: ctx.path,
1399
- model: body.model,
1400
- stream: true,
1401
- toolCount,
1402
- account: "passthrough",
1403
- accountType: "passthrough",
1404
- responseStatus: 500,
1405
- responseTimeMs: Date.now() - requestStartTime,
1406
- errorType: "stream_telemetry_error",
1407
- errorMessage: error instanceof Error ? error.message : String(error),
1408
- });
1351
+ if (!streamFinalized) {
1352
+ finalizeStream(500, "stream_telemetry_error", error instanceof Error ? error.message : String(error));
1353
+ }
1409
1354
  });
1410
1355
  }
1411
1356
  catch {
@@ -1413,25 +1358,7 @@ async function handleClaudePassthroughStreamResponse(args) {
1413
1358
  // the transport failure to the client.
1414
1359
  trackedStream.outcome.then((outcome) => {
1415
1360
  const failure = getStreamFailureDetails(outcome);
1416
- logRequest({
1417
- timestamp: new Date().toISOString(),
1418
- requestId: ctx.requestId,
1419
- method: ctx.method,
1420
- path: ctx.path,
1421
- model: body.model,
1422
- stream: true,
1423
- toolCount,
1424
- account: "passthrough",
1425
- accountType: "passthrough",
1426
- responseStatus: failure?.status ?? response.status,
1427
- responseTimeMs: Date.now() - requestStartTime,
1428
- ...(failure
1429
- ? {
1430
- errorType: failure.errorType,
1431
- errorMessage: failure.message,
1432
- }
1433
- : {}),
1434
- });
1361
+ finalizeStream(failure?.status ?? response.status, failure?.errorType, failure?.message);
1435
1362
  });
1436
1363
  }
1437
1364
  }
@@ -1442,7 +1369,7 @@ async function handleClaudePassthroughStreamResponse(args) {
1442
1369
  });
1443
1370
  }
1444
1371
  async function handleClaudePassthroughJsonResponse(args) {
1445
- const { ctx, body, bodyStr, response, tracer, requestStartTime, toolCount, upstreamSpan, upstreamResponseHeaders, logProxyBody, } = args;
1372
+ const { bodyStr, response, tracer, requestStartTime, upstreamSpan, upstreamResponseHeaders, logProxyBody, logFinalRequest, } = args;
1446
1373
  const responseText = await response.text();
1447
1374
  tracer?.logUpstreamResponseBody(responseText);
1448
1375
  logProxyBody({
@@ -1505,43 +1432,17 @@ async function handleClaudePassthroughJsonResponse(args) {
1505
1432
  tracer.recordBodySizes(bodyStr.length, responseJsonStr.length);
1506
1433
  upstreamSpan?.end();
1507
1434
  tracer.end(response.status, Date.now() - requestStartTime);
1508
- const traceCtx = tracer.getTraceContext();
1509
- logRequest({
1510
- timestamp: new Date().toISOString(),
1511
- requestId: ctx.requestId,
1512
- method: ctx.method,
1513
- path: ctx.path,
1514
- model: body.model,
1515
- stream: false,
1516
- toolCount,
1517
- account: "passthrough",
1518
- accountType: "passthrough",
1519
- responseStatus: response.status,
1520
- responseTimeMs: Date.now() - requestStartTime,
1435
+ logFinalRequest(response.status, "passthrough", "passthrough", undefined, undefined, {
1521
1436
  inputTokens: usage?.input_tokens,
1522
1437
  outputTokens: usage?.output_tokens,
1523
1438
  cacheCreationTokens: usage?.cache_creation_input_tokens,
1524
1439
  cacheReadTokens: usage?.cache_read_input_tokens,
1525
- traceId: traceCtx.traceId,
1526
- spanId: traceCtx.spanId,
1527
1440
  });
1528
1441
  }
1529
1442
  else {
1530
1443
  upstreamSpan?.end();
1531
1444
  tracer?.end(response.status, Date.now() - requestStartTime);
1532
- logRequest({
1533
- timestamp: new Date().toISOString(),
1534
- requestId: ctx.requestId,
1535
- method: ctx.method,
1536
- path: ctx.path,
1537
- model: body.model,
1538
- stream: false,
1539
- toolCount,
1540
- account: "passthrough",
1541
- accountType: "passthrough",
1542
- responseStatus: response.status,
1543
- responseTimeMs: Date.now() - requestStartTime,
1544
- });
1445
+ logFinalRequest(response.status, "passthrough", "passthrough");
1545
1446
  }
1546
1447
  return responseJson;
1547
1448
  }
@@ -1632,7 +1533,9 @@ async function loadClaudeProxyAccounts(args) {
1632
1533
  expiresAt,
1633
1534
  label,
1634
1535
  };
1635
- const refreshed = await refreshToken(tempAccount);
1536
+ const refreshed = await refreshTokenFromLatest(tempAccount, {
1537
+ providerKey: key,
1538
+ });
1636
1539
  if (!refreshed.success) {
1637
1540
  const account = {
1638
1541
  key,
@@ -1701,20 +1604,7 @@ async function loadClaudeProxyAccounts(args) {
1701
1604
  };
1702
1605
  }
1703
1606
  for (const account of accounts) {
1704
- const state = getOrCreateRuntimeState(account.key);
1705
- const tokenChanged = state.lastToken !== account.token ||
1706
- state.lastRefreshToken !== account.refreshToken;
1707
- if (tokenChanged) {
1708
- if (state.permanentlyDisabled) {
1709
- logger.always(`[proxy] account=${account.label} eligible credentials reloaded; clearing stale runtime auth-disable state`);
1710
- }
1711
- // Eligibility was already resolved while loading accounts. This only
1712
- // clears stale in-process auth state after an explicit credential reload.
1713
- state.consecutiveRefreshFailures = 0;
1714
- state.permanentlyDisabled = false;
1715
- }
1716
- state.lastToken = account.token;
1717
- state.lastRefreshToken = account.refreshToken;
1607
+ reconcileEligibleAccountRuntimeState(account);
1718
1608
  }
1719
1609
  await seedRuntimeQuotasFromDisk(accounts);
1720
1610
  const enabledAccounts = accounts.filter((account) => {
@@ -1856,7 +1746,6 @@ async function executeClaudeFallbackTranslation(args) {
1856
1746
  }
1857
1747
  // Telemetry AFTER validation — not before like the old lazy path
1858
1748
  tracer?.end(200, Date.now() - requestStartTime);
1859
- recordFinalSuccess();
1860
1749
  logFinalRequest(200, "", providerLabel, undefined, undefined, {
1861
1750
  inputTokens: resolvedUsage.input,
1862
1751
  outputTokens: resolvedUsage.output,
@@ -1894,7 +1783,6 @@ async function executeClaudeFallbackTranslation(args) {
1894
1783
  toolCalls: streamResult.toolCalls,
1895
1784
  };
1896
1785
  tracer?.end(200, Date.now() - requestStartTime);
1897
- recordFinalSuccess();
1898
1786
  const clientResponse = serializeClaudeResponse(internal, body.model);
1899
1787
  logFinalRequest(200, "", providerLabel, undefined, undefined, {
1900
1788
  inputTokens: internal.usage?.input,
@@ -2100,7 +1988,6 @@ function buildClaudeAnthropicFailureResponse(args) {
2100
1988
  if (invalidRequestFailure) {
2101
1989
  tracer?.setError("invalid_request_error", summarizeErrorMessage(invalidRequestFailure.body));
2102
1990
  tracer?.end(invalidRequestFailure.status, Date.now() - requestStartTime);
2103
- recordFinalError(invalidRequestFailure.status);
2104
1991
  try {
2105
1992
  const parsedError = JSON.parse(invalidRequestFailure.body);
2106
1993
  logFinalRequest(invalidRequestFailure.status, "", "final", "invalid_request_error", summarizeErrorMessage(invalidRequestFailure.body));
@@ -2162,7 +2049,6 @@ function buildClaudeAnthropicFailureResponse(args) {
2162
2049
  const errorBody = buildClaudeError(429, errorMessage, "overloaded_error");
2163
2050
  tracer?.setError("rate_limit_error", errorMessage);
2164
2051
  tracer?.end(429, Date.now() - requestStartTime);
2165
- recordFinalError(429);
2166
2052
  logFinalRequest(429, "", "final", "rate_limit_error", errorMessage);
2167
2053
  const errorBodyText = JSON.stringify(errorBody);
2168
2054
  logProxyBody({
@@ -2241,6 +2127,7 @@ async function handleAnthropicSuccessfulResponse(args) {
2241
2127
  attemptNumber,
2242
2128
  finalBodyStr,
2243
2129
  upstreamSpan,
2130
+ logAttempt,
2244
2131
  logProxyBody,
2245
2132
  logFinalRequest,
2246
2133
  });
@@ -2248,10 +2135,11 @@ async function handleAnthropicSuccessfulResponse(args) {
2248
2135
  async function handleAnthropicStreamingSuccessResponse(args) {
2249
2136
  const { account, accountState, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
2250
2137
  if (!response.body) {
2138
+ recordAttemptError(account.label, account.type, 502);
2139
+ logAttempt(502, "stream_error", "No response body from upstream");
2251
2140
  upstreamSpan?.end();
2252
2141
  tracer?.setError("stream_error", "No response body from upstream");
2253
2142
  tracer?.end(502, Date.now() - requestStartTime);
2254
- recordFinalError(502, account.label, account.type);
2255
2143
  logFinalRequest(502, account.label, account.type, "stream_error", "No response body from upstream");
2256
2144
  const clientError = buildClaudeError(502, "No response body from upstream");
2257
2145
  const clientErrorBody = JSON.stringify(clientError);
@@ -2386,6 +2274,9 @@ async function handleAnthropicStreamingSuccessResponse(args) {
2386
2274
  failure: { message: preflight.message, rateLimit: isRateLimit },
2387
2275
  };
2388
2276
  }
2277
+ logAttempt(response.status, undefined, undefined, {
2278
+ attemptDurationMs: Date.now() - fetchStartMs,
2279
+ });
2389
2280
  const streamOutcomeTracker = createStreamTerminalOutcomeTracker();
2390
2281
  let mainStreamClosed = false;
2391
2282
  const remainingStream = new ReadableStream({
@@ -2462,6 +2353,11 @@ function getStreamFailureDetails(outcome) {
2462
2353
  }
2463
2354
  return undefined;
2464
2355
  }
2356
+ function recordCommittedAnthropicStreamAttemptFailure(outcome, account) {
2357
+ if (outcome.kind === "upstream_error") {
2358
+ recordAttemptError(account.label, account.type, 502);
2359
+ }
2360
+ }
2465
2361
  function attachAnthropicSuccessStreamTelemetry(args) {
2466
2362
  const { account, response, responseHeaders, remainingStream, streamOutcome, tracer, requestStartTime, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
2467
2363
  const { stream: clientCaptureStream, capture: clientCapture } = createRawStreamCapture();
@@ -2480,6 +2376,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2480
2376
  Promise.all([telemetry, clientCapture, streamOutcome])
2481
2377
  .then(([data, clientBody, rawOutcome]) => {
2482
2378
  const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
2379
+ recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account);
2483
2380
  capturedTracer.setUsage({
2484
2381
  inputTokens: data.usage.inputTokens,
2485
2382
  outputTokens: data.usage.outputTokens,
@@ -2519,12 +2416,10 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2519
2416
  if (failure) {
2520
2417
  capturedTracer.setError(failure.errorType, failure.message);
2521
2418
  capturedTracer.end(failure.status, Date.now() - requestStartTime);
2522
- recordFinalError(failure.status, capturedAccountLabel, account.type);
2523
2419
  logFinalRequest(failure.status, capturedAccountLabel, account.type, failure.errorType, failure.message, usage);
2524
2420
  }
2525
2421
  else {
2526
2422
  capturedTracer.end(200, Date.now() - requestStartTime);
2527
- recordFinalSuccess(capturedAccountLabel, account.type);
2528
2423
  logFinalRequest(200, capturedAccountLabel, account.type, undefined, undefined, usage);
2529
2424
  }
2530
2425
  logProxyBody({
@@ -2553,28 +2448,26 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2553
2448
  });
2554
2449
  })
2555
2450
  .catch((error) => {
2556
- capturedTracer.setError("stream_error", error instanceof Error ? error.message : String(error));
2451
+ capturedTracer.setError("stream_telemetry_error", error instanceof Error ? error.message : String(error));
2557
2452
  capturedUpstreamSpan?.end();
2558
2453
  capturedTracer.end(500, Date.now() - requestStartTime);
2559
- recordFinalError(500, capturedAccountLabel, account.type);
2560
- logFinalRequest(500, capturedAccountLabel, account.type, "stream_error", error instanceof Error ? error.message : String(error));
2454
+ logFinalRequest(500, capturedAccountLabel, account.type, "stream_telemetry_error", error instanceof Error ? error.message : String(error));
2561
2455
  });
2562
2456
  }
2563
2457
  catch {
2564
2458
  // Interceptor attachment failed after stream setup. Preserve delivery but
2565
2459
  // still settle the request from the actual stream terminal outcome.
2566
2460
  streamOutcome.then((outcome) => {
2461
+ recordCommittedAnthropicStreamAttemptFailure(outcome, account);
2567
2462
  const failure = getStreamFailureDetails(outcome);
2568
2463
  upstreamSpan?.end();
2569
2464
  if (failure) {
2570
2465
  tracer.setError(failure.errorType, failure.message);
2571
2466
  tracer.end(failure.status, Date.now() - requestStartTime);
2572
- recordFinalError(failure.status, account.label, account.type);
2573
2467
  logFinalRequest(failure.status, account.label, account.type, failure.errorType, failure.message);
2574
2468
  }
2575
2469
  else {
2576
2470
  tracer.end(response.status, Date.now() - requestStartTime);
2577
- recordFinalSuccess(account.label, account.type);
2578
2471
  logFinalRequest(response.status, account.label, account.type);
2579
2472
  }
2580
2473
  });
@@ -2591,6 +2484,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2591
2484
  Promise.all([noTracerTelemetry, clientCapture, streamOutcome])
2592
2485
  .then(([data, clientBody, rawOutcome]) => {
2593
2486
  const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
2487
+ recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account);
2594
2488
  const failure = getStreamFailureDetails(terminalOutcome);
2595
2489
  const usage = {
2596
2490
  inputTokens: data.usage.inputTokens,
@@ -2599,11 +2493,9 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2599
2493
  cacheReadTokens: data.usage.cacheReadInputTokens,
2600
2494
  };
2601
2495
  if (failure) {
2602
- recordFinalError(failure.status, capturedAccountLabel, account.type);
2603
2496
  logFinalRequest(failure.status, capturedAccountLabel, account.type, failure.errorType, failure.message, usage);
2604
2497
  }
2605
2498
  else {
2606
- recordFinalSuccess(capturedAccountLabel, account.type);
2607
2499
  logFinalRequest(200, capturedAccountLabel, account.type, undefined, undefined, usage);
2608
2500
  }
2609
2501
  logProxyBody({
@@ -2633,7 +2525,6 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2633
2525
  })
2634
2526
  .catch((error) => {
2635
2527
  const message = error instanceof Error ? error.message : String(error);
2636
- recordFinalError(500, account.label, account.type);
2637
2528
  logFinalRequest(500, account.label, account.type, "stream_telemetry_error", message);
2638
2529
  });
2639
2530
  }
@@ -2657,13 +2548,12 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2657
2548
  // Non-fatal
2658
2549
  });
2659
2550
  streamOutcome.then((outcome) => {
2551
+ recordCommittedAnthropicStreamAttemptFailure(outcome, account);
2660
2552
  const failure = getStreamFailureDetails(outcome);
2661
2553
  if (failure) {
2662
- recordFinalError(failure.status, account.label, account.type);
2663
2554
  logFinalRequest(failure.status, account.label, account.type, failure.errorType, failure.message);
2664
2555
  }
2665
2556
  else {
2666
- recordFinalSuccess(account.label, account.type);
2667
2557
  logFinalRequest(response.status, account.label, account.type);
2668
2558
  }
2669
2559
  });
@@ -2693,8 +2583,11 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2693
2583
  });
2694
2584
  }
2695
2585
  async function handleAnthropicJsonSuccessResponse(args) {
2696
- const { account, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
2586
+ const { account, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
2697
2587
  const responseText = await response.text();
2588
+ logAttempt(response.status, undefined, undefined, {
2589
+ attemptDurationMs: Date.now() - fetchStartMs,
2590
+ });
2698
2591
  tracer?.logUpstreamResponseBody(responseText);
2699
2592
  logProxyBody({
2700
2593
  phase: "upstream_response",
@@ -2756,7 +2649,6 @@ async function handleAnthropicJsonSuccessResponse(args) {
2756
2649
  tracer.recordBodySizes(finalBodyStr.length, responseJsonStr.length);
2757
2650
  upstreamSpan?.end();
2758
2651
  tracer.end(response.status, Date.now() - requestStartTime);
2759
- recordFinalSuccess(account.label, account.type);
2760
2652
  logFinalRequest(response.status, account.label, account.type, undefined, undefined, {
2761
2653
  inputTokens: usage?.input_tokens,
2762
2654
  outputTokens: usage?.output_tokens,
@@ -2769,7 +2661,6 @@ async function handleAnthropicJsonSuccessResponse(args) {
2769
2661
  const noTracerUsage = responseJson && typeof responseJson === "object"
2770
2662
  ? responseJson.usage
2771
2663
  : undefined;
2772
- recordFinalSuccess(account.label, account.type);
2773
2664
  logFinalRequest(response.status, account.label, account.type, undefined, undefined, {
2774
2665
  inputTokens: noTracerUsage?.input_tokens,
2775
2666
  outputTokens: noTracerUsage?.output_tokens,
@@ -2779,8 +2670,8 @@ async function handleAnthropicJsonSuccessResponse(args) {
2779
2670
  }
2780
2671
  return { response: responseJson };
2781
2672
  }
2782
- async function handleAnthropicSuccessfulRetryResponse(args) {
2783
- const { body, account, accountState, retryResp, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
2673
+ async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
2674
+ const { account, accountState, retryResp, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
2784
2675
  const retryQuota = parseQuotaHeaders(retryResp.headers);
2785
2676
  if (retryQuota) {
2786
2677
  // Keep the auth-retry success path in parity with the main success path:
@@ -2802,63 +2693,11 @@ async function handleAnthropicSuccessfulRetryResponse(args) {
2802
2693
  });
2803
2694
  });
2804
2695
  }
2805
- if (body.stream && retryResp.body) {
2806
- const retryReader = retryResp.body.getReader();
2807
- const streamOutcomeTracker = createStreamTerminalOutcomeTracker();
2808
- let retryStreamClosed = false;
2809
- const retryStream = new ReadableStream({
2810
- async pull(controller) {
2811
- if (retryStreamClosed) {
2812
- return;
2813
- }
2814
- try {
2815
- const { done, value } = await retryReader.read();
2816
- if (retryStreamClosed) {
2817
- return;
2818
- }
2819
- if (done) {
2820
- retryStreamClosed = true;
2821
- streamOutcomeTracker.complete();
2822
- controller.close();
2823
- return;
2824
- }
2825
- controller.enqueue(value);
2826
- }
2827
- catch (streamErr) {
2828
- const errMsg = describeTransportError(streamErr);
2829
- logger.always(`[proxy] mid-stream error (auth-retry) account=${account.label}: ${errMsg}`);
2830
- streamOutcomeTracker.fail(errMsg);
2831
- if (!retryStreamClosed) {
2832
- retryStreamClosed = true;
2833
- const errorEvent = `event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: `Upstream stream interrupted: ${errMsg}` } })}\n\n`;
2834
- controller.enqueue(new TextEncoder().encode(errorEvent));
2835
- controller.close();
2836
- }
2837
- }
2838
- },
2839
- cancel() {
2840
- retryStreamClosed = true;
2841
- streamOutcomeTracker.cancel();
2842
- return retryReader.cancel();
2843
- },
2844
- });
2845
- return attachAnthropicSuccessStreamTelemetry({
2846
- account,
2847
- response: retryResp,
2848
- responseHeaders: Object.fromEntries([...retryResp.headers.entries()]),
2849
- remainingStream: retryStream,
2850
- streamOutcome: streamOutcomeTracker.outcome,
2851
- tracer,
2852
- requestStartTime,
2853
- attemptNumber,
2854
- finalBodyStr,
2855
- upstreamSpan,
2856
- logProxyBody,
2857
- logFinalRequest,
2858
- });
2859
- }
2860
2696
  const retryRespHeaders = Object.fromEntries([...retryResp.headers.entries()]);
2861
2697
  const retryText = await retryResp.text();
2698
+ logAttempt(retryResp.status, undefined, undefined, {
2699
+ attemptDurationMs: Date.now() - fetchStartMs,
2700
+ });
2862
2701
  tracer?.logUpstreamResponseHeaders(retryRespHeaders);
2863
2702
  tracer?.logUpstreamResponseBody(retryText);
2864
2703
  logProxyBody({
@@ -2902,7 +2741,6 @@ async function handleAnthropicSuccessfulRetryResponse(args) {
2902
2741
  tracer.recordBodySizes(finalBodyStr.length, retryJsonStr.length);
2903
2742
  upstreamSpan?.end();
2904
2743
  tracer.end(retryResp.status, Date.now() - requestStartTime);
2905
- recordFinalSuccess(account.label, account.type);
2906
2744
  logFinalRequest(retryResp.status, account.label, account.type, undefined, undefined, {
2907
2745
  inputTokens: retryUsage?.input_tokens,
2908
2746
  outputTokens: retryUsage?.output_tokens,
@@ -2912,7 +2750,6 @@ async function handleAnthropicSuccessfulRetryResponse(args) {
2912
2750
  }
2913
2751
  else {
2914
2752
  upstreamSpan?.end();
2915
- recordFinalSuccess(account.label, account.type);
2916
2753
  logFinalRequest(retryResp.status, account.label, account.type);
2917
2754
  }
2918
2755
  return retryJson;
@@ -2933,7 +2770,7 @@ async function handleAnthropicAuthRetry(args) {
2933
2770
  let authRetryError = "received 401 from Anthropic";
2934
2771
  for (let authRetry = 0; authRetry < MAX_AUTH_RETRIES; authRetry++) {
2935
2772
  logger.always(`[proxy] ← 401 account=${account.label} refreshing (attempt ${authRetry + 1}/${MAX_AUTH_RETRIES})`);
2936
- const refreshSucceeded = await refreshToken(account);
2773
+ const refreshSucceeded = await refreshTokenFromLatest(account, account.persistTarget);
2937
2774
  if (!refreshSucceeded.success) {
2938
2775
  authRetryError = `refresh failed for account=${account.label} attempt ${authRetry + 1}/${MAX_AUTH_RETRIES}: ${refreshSucceeded.error?.slice(0, 200) ?? "unknown"}`;
2939
2776
  currentLastError = authRetryError;
@@ -2960,7 +2797,7 @@ async function handleAnthropicAuthRetry(args) {
2960
2797
  const retryLogAttempt = (status, errorType, errorMessage, extra) => logAttempt(status, errorType, errorMessage, {
2961
2798
  ...extra,
2962
2799
  attempt: retryAttemptNumber,
2963
- attemptDurationMs: Date.now() - retryAttemptStartedAt,
2800
+ attemptDurationMs: extra?.attemptDurationMs ?? Date.now() - retryAttemptStartedAt,
2964
2801
  });
2965
2802
  const retryBodyStr = buildUpstreamBody(account.token).bodyStr;
2966
2803
  const retryFetchStartMs = Date.now();
@@ -2986,23 +2823,54 @@ async function handleAnthropicAuthRetry(args) {
2986
2823
  authRetrySucceeded = true;
2987
2824
  accountState.consecutiveRefreshFailures = 0;
2988
2825
  logger.always(`[proxy] ← 200 account=${account.label} (after ${authRetry + 1} refresh(es))`);
2989
- const successResponse = await handleAnthropicSuccessfulRetryResponse({
2990
- ctx,
2991
- body,
2992
- account,
2993
- accountState,
2994
- retryResp,
2995
- tracer,
2996
- requestStartTime,
2997
- fetchStartMs: retryFetchStartMs,
2998
- attemptNumber: retryAttemptNumber,
2999
- finalBodyStr: retryBodyStr,
3000
- upstreamSpan: currentUpstreamSpan,
3001
- logProxyBody,
3002
- logFinalRequest,
3003
- });
2826
+ const successResult = body.stream
2827
+ ? await handleAnthropicSuccessfulResponse({
2828
+ ctx,
2829
+ body,
2830
+ account,
2831
+ accountState,
2832
+ response: retryResp,
2833
+ tracer,
2834
+ requestStartTime,
2835
+ fetchStartMs: retryFetchStartMs,
2836
+ attemptNumber: retryAttemptNumber,
2837
+ finalBodyStr: retryBodyStr,
2838
+ upstreamSpan: currentUpstreamSpan,
2839
+ logAttempt: retryLogAttempt,
2840
+ logProxyBody,
2841
+ logFinalRequest,
2842
+ })
2843
+ : {
2844
+ response: await handleAnthropicSuccessfulNonStreamRetryResponse({
2845
+ account,
2846
+ accountState,
2847
+ retryResp,
2848
+ tracer,
2849
+ requestStartTime,
2850
+ fetchStartMs: retryFetchStartMs,
2851
+ attemptNumber: retryAttemptNumber,
2852
+ finalBodyStr: retryBodyStr,
2853
+ upstreamSpan: currentUpstreamSpan,
2854
+ logAttempt: retryLogAttempt,
2855
+ logProxyBody,
2856
+ logFinalRequest,
2857
+ }),
2858
+ };
2859
+ if ("retryNextAccount" in successResult) {
2860
+ const failure = successResult.failure;
2861
+ return {
2862
+ continueLoop: true,
2863
+ lastError: failure?.message ?? currentLastError,
2864
+ authFailureMessage: currentAuthFailureMessage,
2865
+ sawRateLimit: currentSawRateLimit || Boolean(failure?.rateLimit),
2866
+ sawTransientFailure: currentSawTransientFailure ||
2867
+ Boolean(failure && !failure.rateLimit),
2868
+ sawNetworkError: currentSawNetworkError,
2869
+ upstreamSpan: undefined,
2870
+ };
2871
+ }
3004
2872
  return {
3005
- response: successResponse,
2873
+ response: successResult.response,
3006
2874
  continueLoop: false,
3007
2875
  lastError: currentLastError,
3008
2876
  authFailureMessage: currentAuthFailureMessage,
@@ -3119,7 +2987,6 @@ async function handleAnthropicAuthRetry(args) {
3119
2987
  break;
3120
2988
  }
3121
2989
  retryLogAttempt(retryStatus, "api_error", summarizeErrorMessage(retryBody));
3122
- recordFinalError(retryStatus, account.label, account.type);
3123
2990
  try {
3124
2991
  logFinalRequest(retryStatus, account.label, account.type, "api_error", summarizeErrorMessage(retryBody));
3125
2992
  return {
@@ -3225,7 +3092,6 @@ function buildAnthropicTerminalErrorResponse(args) {
3225
3092
  }
3226
3093
  function finalizeAnthropicTerminalFetchError(args) {
3227
3094
  const { terminalError, account, tracer, requestStartTime, attemptNumber, logProxyBody, logFinalRequest, } = args;
3228
- recordFinalError(terminalError.status, account.label, account.type);
3229
3095
  tracer?.end(terminalError.status, Date.now() - requestStartTime);
3230
3096
  return buildAnthropicTerminalErrorResponse({
3231
3097
  responseStatus: terminalError.status,
@@ -3329,7 +3195,6 @@ async function handleAnthropicNonOkResponse(args) {
3329
3195
  };
3330
3196
  }
3331
3197
  if (response.status === 404) {
3332
- recordFinalError(response.status, account.label, account.type);
3333
3198
  logger.always(`[proxy] ← 404 account=${account.label}`);
3334
3199
  logAttempt(404, "not_found_error", summarizeErrorMessage(errBody));
3335
3200
  tracer?.setError("not_found_error", summarizeErrorMessage(errBody));
@@ -3378,7 +3243,6 @@ async function handleAnthropicNonOkResponse(args) {
3378
3243
  upstreamSpan: undefined,
3379
3244
  };
3380
3245
  }
3381
- recordFinalError(response.status, account.label, account.type);
3382
3246
  logger.always(`[proxy] ← ${response.status} account=${account.label}`);
3383
3247
  logger.debug(`[claude-proxy] error body: ${errBody.substring(0, 200)}`);
3384
3248
  logAttempt(response.status, "api_error", summarizeErrorMessage(errBody));
@@ -3449,7 +3313,35 @@ function createClaudeRequestRuntimeContext(args) {
3449
3313
  : {}),
3450
3314
  });
3451
3315
  };
3316
+ let finalRequestLogged = false;
3452
3317
  const logFinalRequest = (status, accountLabel, accountType, errorType, errorMessage, extra) => {
3318
+ if (finalRequestLogged) {
3319
+ logger.debug(`[claude-proxy] ignored duplicate finalization for request ${ctx.requestId}`);
3320
+ return;
3321
+ }
3322
+ finalRequestLogged = true;
3323
+ const finalAccountLabel = accountLabel ||
3324
+ (status >= 400 ? PROXY_INTERNAL_ACCOUNT_LABEL : undefined);
3325
+ const finalAccountType = accountLabel
3326
+ ? accountType || undefined
3327
+ : status >= 400
3328
+ ? PROXY_INTERNAL_ACCOUNT_TYPE
3329
+ : undefined;
3330
+ if (status >= 400) {
3331
+ recordFinalError(status, finalAccountLabel, finalAccountType, {
3332
+ requestId: ctx.requestId,
3333
+ errorType,
3334
+ terminalOutcome: errorType === "client_cancelled"
3335
+ ? "client_cancelled"
3336
+ : errorType?.includes("stream")
3337
+ ? "stream_error"
3338
+ : "handler_error",
3339
+ message: errorMessage,
3340
+ });
3341
+ }
3342
+ else {
3343
+ recordFinalSuccess(finalAccountLabel, finalAccountType);
3344
+ }
3453
3345
  const traceCtx = tracer?.getTraceContext();
3454
3346
  logRequest({
3455
3347
  timestamp: new Date().toISOString(),
@@ -3459,8 +3351,8 @@ function createClaudeRequestRuntimeContext(args) {
3459
3351
  model: body.model,
3460
3352
  stream: !!body.stream,
3461
3353
  toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
3462
- account: accountLabel,
3463
- accountType,
3354
+ account: finalAccountLabel ?? "",
3355
+ accountType: finalAccountType ?? "",
3464
3356
  responseStatus: status,
3465
3357
  responseTimeMs: Date.now() - requestStartTime,
3466
3358
  ...(errorType ? { errorType } : {}),
@@ -3485,7 +3377,6 @@ function createClaudeRequestRuntimeContext(args) {
3485
3377
  const buildLoggedClaudeError = (status, message, errorType, extra) => {
3486
3378
  const errorBody = buildClaudeError(status, message, errorType);
3487
3379
  const errorBodyText = JSON.stringify(errorBody);
3488
- recordFinalError(status, extra?.account, extra?.accountType);
3489
3380
  logFinalRequest(status, extra?.account ?? "", extra?.accountType ?? "final", errorType, message);
3490
3381
  logProxyBody({
3491
3382
  phase: "client_response",
@@ -3565,7 +3456,7 @@ async function prepareAnthropicAccountAttempt(args) {
3565
3456
  let lastError = currentLastError;
3566
3457
  let authFailureMessage = currentAuthFailureMessage;
3567
3458
  if (needsRefresh(account)) {
3568
- const refreshed = await refreshToken(account);
3459
+ const refreshed = await refreshTokenFromLatest(account, account.persistTarget);
3569
3460
  if (refreshed.success) {
3570
3461
  if (account.persistTarget) {
3571
3462
  await persistTokens(account.persistTarget, account);
@@ -4274,6 +4165,31 @@ function isClaudeProxyRouteRuntimeOptions(value) {
4274
4165
  "runtimeConfigProvider" in value &&
4275
4166
  typeof value.runtimeConfigProvider === "function");
4276
4167
  }
4168
+ function buildEarlyClaudeRequestError(args) {
4169
+ const { ctx, body, status, message, errorType } = args;
4170
+ recordFinalError(status, PROXY_INTERNAL_ACCOUNT_LABEL, PROXY_INTERNAL_ACCOUNT_TYPE, {
4171
+ requestId: ctx.requestId,
4172
+ errorType,
4173
+ terminalOutcome: "handler_error",
4174
+ message,
4175
+ });
4176
+ void logRequest({
4177
+ timestamp: new Date().toISOString(),
4178
+ requestId: ctx.requestId,
4179
+ method: ctx.method,
4180
+ path: ctx.path,
4181
+ model: typeof body?.model === "string" ? body.model : "",
4182
+ stream: body?.stream ?? false,
4183
+ toolCount: Array.isArray(body?.tools) ? body.tools.length : 0,
4184
+ account: PROXY_INTERNAL_ACCOUNT_LABEL,
4185
+ accountType: PROXY_INTERNAL_ACCOUNT_TYPE,
4186
+ responseStatus: status,
4187
+ responseTimeMs: 0,
4188
+ errorType,
4189
+ errorMessage: message,
4190
+ });
4191
+ return buildClaudeError(status, message);
4192
+ }
4277
4193
  /**
4278
4194
  * Create Claude-compatible proxy routes.
4279
4195
  *
@@ -4317,15 +4233,27 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
4317
4233
  // 1. Validate
4318
4234
  if (typeof body?.model !== "string" ||
4319
4235
  !Array.isArray(body?.messages)) {
4320
- return buildClaudeError(400, "Missing required fields: model, messages");
4236
+ return buildEarlyClaudeRequestError({
4237
+ ctx,
4238
+ body,
4239
+ status: 400,
4240
+ message: "Missing required fields: model, messages",
4241
+ errorType: "invalid_request_error",
4242
+ });
4321
4243
  }
4322
4244
  // 2. Resolve model via router (or pass through to anthropic)
4323
4245
  // Guard: without a model router, only Claude models are allowed.
4324
4246
  const modelLower = body.model.toLowerCase();
4325
4247
  if (!requestModelRouter && !modelLower.startsWith("claude-")) {
4326
- return buildClaudeError(404, `Model '${body.model}' is not an Anthropic model. ` +
4327
- `The proxy only supports Claude models. ` +
4328
- `Use a model router to route non-Claude models to other providers.`);
4248
+ return buildEarlyClaudeRequestError({
4249
+ ctx,
4250
+ body,
4251
+ status: 404,
4252
+ message: `Model '${body.model}' is not an Anthropic model. ` +
4253
+ `The proxy only supports Claude models. ` +
4254
+ `Use a model router to route non-Claude models to other providers.`,
4255
+ errorType: "not_found_error",
4256
+ });
4329
4257
  }
4330
4258
  const route = requestModelRouter?.resolve(body.model) ?? {
4331
4259
  provider: "anthropic",
@@ -4355,6 +4283,7 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
4355
4283
  tracer,
4356
4284
  requestStartTime,
4357
4285
  logProxyBody,
4286
+ logFinalRequest,
4358
4287
  });
4359
4288
  }
4360
4289
  return handleAnthropicRoutedClaudeRequest({
@@ -4476,17 +4405,51 @@ function getOrCreateRuntimeState(accountKey) {
4476
4405
  accountRuntimeState.set(accountKey, initial);
4477
4406
  return initial;
4478
4407
  }
4408
+ function reconcileEligibleAccountRuntimeState(account) {
4409
+ const state = getOrCreateRuntimeState(account.key);
4410
+ const tokenChanged = state.lastToken !== account.token ||
4411
+ state.lastRefreshToken !== account.refreshToken;
4412
+ const wasPermanentlyDisabled = state.permanentlyDisabled;
4413
+ if (wasPermanentlyDisabled) {
4414
+ logger.always(`[proxy] account=${account.label} is enabled in the token store; clearing stale runtime auth-disable state`);
4415
+ }
4416
+ state.permanentlyDisabled = false;
4417
+ if (tokenChanged || wasPermanentlyDisabled) {
4418
+ state.consecutiveRefreshFailures = 0;
4419
+ }
4420
+ state.lastToken = account.token;
4421
+ state.lastRefreshToken = account.refreshToken;
4422
+ }
4479
4423
  async function disableAccountUntilReauth(account, state, reason) {
4480
- state.permanentlyDisabled = true;
4481
- // Decision 7 (usage): Persist disabled state to disk so it survives restarts
4482
4424
  try {
4483
4425
  const { tokenStore } = await import("../../auth/tokenStore.js");
4484
- await tokenStore.markDisabled(account.key, reason);
4426
+ const providerKey = account.persistTarget &&
4427
+ typeof account.persistTarget !== "string" &&
4428
+ "providerKey" in account.persistTarget
4429
+ ? account.persistTarget.providerKey
4430
+ : undefined;
4431
+ if (providerKey) {
4432
+ const disabled = await tokenStore.markDisabledIfCurrent(providerKey, {
4433
+ accessToken: account.token,
4434
+ refreshToken: account.refreshToken,
4435
+ expiresAt: account.expiresAt ?? 0,
4436
+ }, reason);
4437
+ if (!disabled) {
4438
+ state.permanentlyDisabled = false;
4439
+ logger.always(`[proxy] account=${account.label} credentials changed while authentication was in flight; ignored stale disable`);
4440
+ return false;
4441
+ }
4442
+ }
4443
+ else {
4444
+ await tokenStore.markDisabled(account.key, reason);
4445
+ }
4485
4446
  }
4486
4447
  catch (e) {
4487
4448
  logger.debug(`[proxy] failed to persist disabled state for ${account.label}: ${e instanceof Error ? e.message : String(e)}`);
4488
4449
  }
4450
+ state.permanentlyDisabled = true;
4489
4451
  logger.always(`[proxy] account=${account.label} disabled until re-authentication. Run: neurolink auth login anthropic --method oauth`);
4452
+ return true;
4490
4453
  }
4491
4454
  async function coolAccountAfterTransientRefreshFailure(account, state) {
4492
4455
  state.consecutiveRefreshFailures += 1;
@@ -4710,6 +4673,7 @@ export const __testHooks = {
4710
4673
  orderAccountsByQuota,
4711
4674
  resetEpochToMs,
4712
4675
  seedRuntimeQuotasFromDisk,
4676
+ reconcileEligibleAccountRuntimeState,
4713
4677
  getAccountRuntimeState: (key) => {
4714
4678
  const state = accountRuntimeState.get(key);
4715
4679
  return state ? { ...state } : undefined;