@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.
- package/CHANGELOG.md +12 -0
- package/dist/auth/tokenStore.d.ts +7 -0
- package/dist/auth/tokenStore.js +54 -0
- package/dist/browser/neurolink.min.js +377 -380
- package/dist/cli/commands/proxy.d.ts +2 -1
- package/dist/cli/commands/proxy.js +290 -63
- package/dist/cli/commands/proxyAnalyze.js +10 -1
- package/dist/lib/auth/tokenStore.d.ts +7 -0
- package/dist/lib/auth/tokenStore.js +54 -0
- package/dist/lib/proxy/proxyAnalysis.js +136 -12
- package/dist/lib/proxy/proxyTranslationEngine.d.ts +1 -0
- package/dist/lib/proxy/proxyTranslationEngine.js +56 -12
- package/dist/lib/proxy/rawStreamCapture.js +47 -1
- package/dist/lib/proxy/requestLogger.d.ts +2 -0
- package/dist/lib/proxy/requestLogger.js +72 -13
- package/dist/lib/proxy/rollingProxyServer.js +79 -8
- package/dist/lib/proxy/rollingWorkerProcess.js +20 -15
- package/dist/lib/proxy/rollingWorkerProtocol.js +3 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.js +31 -11
- package/dist/lib/proxy/runtimeConfig.d.ts +1 -0
- package/dist/lib/proxy/runtimeConfig.js +20 -5
- package/dist/lib/proxy/socketWorkerRuntime.js +41 -13
- package/dist/lib/proxy/sseInterceptor.js +47 -1
- package/dist/lib/proxy/tokenRefresh.d.ts +6 -0
- package/dist/lib/proxy/tokenRefresh.js +70 -15
- package/dist/lib/proxy/usageStats.d.ts +25 -3
- package/dist/lib/proxy/usageStats.js +546 -55
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +261 -297
- package/dist/lib/types/proxy.d.ts +90 -1
- package/dist/proxy/proxyAnalysis.js +136 -12
- package/dist/proxy/proxyTranslationEngine.d.ts +1 -0
- package/dist/proxy/proxyTranslationEngine.js +56 -12
- package/dist/proxy/rawStreamCapture.js +47 -1
- package/dist/proxy/requestLogger.d.ts +2 -0
- package/dist/proxy/requestLogger.js +72 -13
- package/dist/proxy/rollingProxyServer.js +79 -8
- package/dist/proxy/rollingWorkerProcess.js +20 -15
- package/dist/proxy/rollingWorkerProtocol.js +3 -0
- package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/proxy/rollingWorkerSupervisor.js +31 -11
- package/dist/proxy/runtimeConfig.d.ts +1 -0
- package/dist/proxy/runtimeConfig.js +20 -5
- package/dist/proxy/socketWorkerRuntime.js +41 -13
- package/dist/proxy/sseInterceptor.js +47 -1
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +70 -15
- package/dist/proxy/usageStats.d.ts +25 -3
- package/dist/proxy/usageStats.js +546 -55
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -0
- package/dist/server/routes/claudeProxyRoutes.js +261 -297
- package/dist/types/proxy.d.ts +90 -1
- 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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
|
2783
|
-
const {
|
|
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
|
|
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
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
4327
|
-
|
|
4328
|
-
|
|
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
|
-
|
|
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;
|