@juspay/neurolink 9.92.3 → 9.93.1
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/browser/neurolink.min.js +379 -382
- package/dist/cli/commands/proxy.d.ts +5 -3
- package/dist/cli/commands/proxy.js +360 -69
- package/dist/cli/commands/proxyAnalyze.d.ts +3 -0
- package/dist/cli/commands/proxyAnalyze.js +147 -0
- package/dist/cli/parser.js +3 -1
- package/dist/lib/proxy/proxyActivity.d.ts +2 -2
- package/dist/lib/proxy/proxyActivity.js +49 -9
- package/dist/lib/proxy/proxyAnalysis.d.ts +3 -0
- package/dist/lib/proxy/proxyAnalysis.js +454 -0
- package/dist/lib/proxy/proxyHealth.d.ts +4 -0
- package/dist/lib/proxy/proxyHealth.js +21 -0
- package/dist/lib/proxy/proxyLifecycle.d.ts +8 -0
- package/dist/lib/proxy/proxyLifecycle.js +333 -0
- package/dist/lib/proxy/requestLogger.js +6 -0
- package/dist/lib/proxy/sseInterceptor.d.ts +9 -1
- package/dist/lib/proxy/sseInterceptor.js +6 -5
- package/dist/lib/proxy/streamOutcome.d.ts +3 -1
- package/dist/lib/proxy/streamOutcome.js +77 -0
- package/dist/lib/proxy/updateCoordinator.d.ts +7 -0
- package/dist/lib/proxy/updateCoordinator.js +60 -0
- package/dist/lib/proxy/updateState.d.ts +4 -0
- package/dist/lib/proxy/updateState.js +51 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +137 -35
- package/dist/lib/types/cli.d.ts +7 -0
- package/dist/lib/types/proxy.d.ts +269 -0
- package/dist/proxy/proxyActivity.d.ts +2 -2
- package/dist/proxy/proxyActivity.js +49 -9
- package/dist/proxy/proxyAnalysis.d.ts +3 -0
- package/dist/proxy/proxyAnalysis.js +453 -0
- package/dist/proxy/proxyHealth.d.ts +4 -0
- package/dist/proxy/proxyHealth.js +21 -0
- package/dist/proxy/proxyLifecycle.d.ts +8 -0
- package/dist/proxy/proxyLifecycle.js +332 -0
- package/dist/proxy/requestLogger.js +6 -0
- package/dist/proxy/sseInterceptor.d.ts +9 -1
- package/dist/proxy/sseInterceptor.js +6 -5
- package/dist/proxy/streamOutcome.d.ts +3 -1
- package/dist/proxy/streamOutcome.js +77 -0
- package/dist/proxy/updateCoordinator.d.ts +7 -0
- package/dist/proxy/updateCoordinator.js +59 -0
- package/dist/proxy/updateState.d.ts +4 -0
- package/dist/proxy/updateState.js +51 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +137 -35
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/proxy.d.ts +269 -0
- package/package.json +4 -1
|
@@ -25,7 +25,7 @@ import { createRawStreamCapture } from "../../proxy/rawStreamCapture.js";
|
|
|
25
25
|
import { relocateClientSystemIntoMessages } from "../../proxy/systemRelocation.js";
|
|
26
26
|
import { logBodyCapture, logRequest, logRequestAttempt, } from "../../proxy/requestLogger.js";
|
|
27
27
|
import { createSSEInterceptor } from "../../proxy/sseInterceptor.js";
|
|
28
|
-
import { createStreamTerminalOutcomeTracker, mergeStreamTerminalOutcome, } from "../../proxy/streamOutcome.js";
|
|
28
|
+
import { createStreamTerminalOutcomeTracker, mergeStreamTerminalOutcome, preflightAnthropicStream, } from "../../proxy/streamOutcome.js";
|
|
29
29
|
import { isPermanentRefreshFailure, needsRefresh, persistTokens, refreshToken, } from "../../proxy/tokenRefresh.js";
|
|
30
30
|
import { buildProxyTranslationPlan, parseRetryAfterMs, } from "../../proxy/routingPolicy.js";
|
|
31
31
|
import { writeJsonSnapshotAtomically } from "../../proxy/snapshotPersistence.js";
|
|
@@ -2182,7 +2182,7 @@ function buildClaudeAnthropicFailureResponse(args) {
|
|
|
2182
2182
|
});
|
|
2183
2183
|
}
|
|
2184
2184
|
async function handleAnthropicSuccessfulResponse(args) {
|
|
2185
|
-
const { ctx, body, account, accountState, response, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
|
|
2185
|
+
const { ctx, body, account, accountState, response, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
|
|
2186
2186
|
accountState.consecutiveRefreshFailures = 0;
|
|
2187
2187
|
logger.always(`[proxy] ← ${response.status} account=${account.label}`);
|
|
2188
2188
|
const quota = parseQuotaHeaders(response.headers);
|
|
@@ -2222,6 +2222,7 @@ async function handleAnthropicSuccessfulResponse(args) {
|
|
|
2222
2222
|
attemptNumber,
|
|
2223
2223
|
finalBodyStr,
|
|
2224
2224
|
upstreamSpan,
|
|
2225
|
+
logAttempt,
|
|
2225
2226
|
logProxyBody,
|
|
2226
2227
|
logFinalRequest,
|
|
2227
2228
|
});
|
|
@@ -2241,7 +2242,7 @@ async function handleAnthropicSuccessfulResponse(args) {
|
|
|
2241
2242
|
});
|
|
2242
2243
|
}
|
|
2243
2244
|
async function handleAnthropicStreamingSuccessResponse(args) {
|
|
2244
|
-
const { account, accountState
|
|
2245
|
+
const { account, accountState, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
|
|
2245
2246
|
if (!response.body) {
|
|
2246
2247
|
upstreamSpan?.end();
|
|
2247
2248
|
tracer?.setError("stream_error", "No response body from upstream");
|
|
@@ -2265,44 +2266,129 @@ async function handleAnthropicStreamingSuccessResponse(args) {
|
|
|
2265
2266
|
return { response: clientError };
|
|
2266
2267
|
}
|
|
2267
2268
|
const reader = response.body.getReader();
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
catch (error) {
|
|
2273
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2269
|
+
const preflight = await preflightAnthropicStream(reader);
|
|
2270
|
+
if (preflight.kind === "transport_error") {
|
|
2271
|
+
const message = describeTransportError(preflight.error);
|
|
2272
|
+
const partialBody = Buffer.concat(preflight.chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
2274
2273
|
logger.always(`[proxy] stream failed before first chunk account=${account.label}: ${message}; trying next account`);
|
|
2275
2274
|
recordAttemptError(account.label, account.type, 502);
|
|
2275
|
+
logAttempt(502, "stream_error", message, { retryable: true });
|
|
2276
2276
|
tracer?.recordRetry(account.label, "stream_before_first_chunk");
|
|
2277
2277
|
upstreamSpan?.end();
|
|
2278
2278
|
logProxyBody({
|
|
2279
2279
|
phase: "upstream_response",
|
|
2280
2280
|
headers: responseHeaders,
|
|
2281
|
-
body:
|
|
2282
|
-
bodySize: Buffer.byteLength(
|
|
2281
|
+
body: partialBody,
|
|
2282
|
+
bodySize: Buffer.byteLength(partialBody, "utf8"),
|
|
2283
2283
|
contentType: responseHeaders["content-type"] ?? "text/event-stream",
|
|
2284
2284
|
account: account.label,
|
|
2285
2285
|
accountType: account.type,
|
|
2286
2286
|
attempt: attemptNumber,
|
|
2287
|
-
responseStatus:
|
|
2287
|
+
responseStatus: response.status,
|
|
2288
2288
|
durationMs: Date.now() - fetchStartMs,
|
|
2289
|
+
metadata: { logicalStatus: 502, transportError: message },
|
|
2289
2290
|
});
|
|
2290
|
-
return {
|
|
2291
|
+
return {
|
|
2292
|
+
retryNextAccount: true,
|
|
2293
|
+
failure: { message, rateLimit: false },
|
|
2294
|
+
};
|
|
2291
2295
|
}
|
|
2292
|
-
if (
|
|
2296
|
+
if (preflight.kind === "empty") {
|
|
2293
2297
|
await reader.cancel().catch(() => {
|
|
2294
2298
|
// Best-effort release before rotating to another account.
|
|
2295
2299
|
});
|
|
2296
2300
|
logger.always(`[proxy] ← empty stream from account=${account.label}, trying next`);
|
|
2301
|
+
recordAttemptError(account.label, account.type, 502);
|
|
2302
|
+
logAttempt(502, "empty_stream", "Empty upstream stream", {
|
|
2303
|
+
retryable: true,
|
|
2304
|
+
});
|
|
2297
2305
|
tracer?.recordRetry(account.label, "empty_stream");
|
|
2298
2306
|
upstreamSpan?.end();
|
|
2299
|
-
|
|
2307
|
+
logProxyBody({
|
|
2308
|
+
phase: "upstream_response",
|
|
2309
|
+
headers: responseHeaders,
|
|
2310
|
+
body: "",
|
|
2311
|
+
bodySize: 0,
|
|
2312
|
+
contentType: responseHeaders["content-type"] ?? "text/event-stream",
|
|
2313
|
+
account: account.label,
|
|
2314
|
+
accountType: account.type,
|
|
2315
|
+
attempt: attemptNumber,
|
|
2316
|
+
responseStatus: response.status,
|
|
2317
|
+
durationMs: Date.now() - fetchStartMs,
|
|
2318
|
+
metadata: { logicalStatus: 502, upstreamErrorType: "empty_stream" },
|
|
2319
|
+
});
|
|
2320
|
+
return {
|
|
2321
|
+
retryNextAccount: true,
|
|
2322
|
+
failure: { message: "Empty upstream stream", rateLimit: false },
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
if (preflight.kind === "sse_error") {
|
|
2326
|
+
await reader.cancel().catch(() => {
|
|
2327
|
+
// Best-effort release before rotating to another account.
|
|
2328
|
+
});
|
|
2329
|
+
const bodyText = Buffer.concat(preflight.chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
2330
|
+
const isRateLimit = preflight.errorType === "rate_limit_error";
|
|
2331
|
+
const logicalStatus = isRateLimit ? 429 : 502;
|
|
2332
|
+
const quota = parseQuotaHeaders(responseHeaders);
|
|
2333
|
+
const now = Date.now();
|
|
2334
|
+
if (isRateLimit) {
|
|
2335
|
+
const cooldownPlan = planCooldownFor429(quota, parseRetryAfterMs(responseHeaders["retry-after"] ?? null), now, getUnifiedRateLimitStatus(responseHeaders));
|
|
2336
|
+
accountState.quota = quota ?? accountState.quota;
|
|
2337
|
+
const rateLimitKind = cooldownPlan.reason === "transient" ? "transient" : "quota";
|
|
2338
|
+
if (!accountState.coolingUntil ||
|
|
2339
|
+
cooldownPlan.coolingUntil > accountState.coolingUntil) {
|
|
2340
|
+
accountState.coolingUntil = cooldownPlan.coolingUntil;
|
|
2341
|
+
accountState.coolingReason = cooldownPlan.reason;
|
|
2342
|
+
await saveAccountCooldown(account.key, cooldownPlan.coolingUntil, cooldownPlan.reason).catch(() => {
|
|
2343
|
+
// Non-fatal: routing already has the in-memory cooldown.
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
recordAttemptError(account.label, account.type, 429, rateLimitKind);
|
|
2347
|
+
logAttempt(429, "rate_limit_error", preflight.message, {
|
|
2348
|
+
retryable: true,
|
|
2349
|
+
rateLimitKind,
|
|
2350
|
+
cooldownReason: cooldownPlan.reason,
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
else {
|
|
2354
|
+
recordAttemptError(account.label, account.type, logicalStatus);
|
|
2355
|
+
logAttempt(logicalStatus, preflight.errorType, preflight.message, {
|
|
2356
|
+
retryable: true,
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
logger.always(`[proxy] immediate SSE ${preflight.errorType} account=${account.label}: ${preflight.message}; rotating before client commit`);
|
|
2360
|
+
tracer?.recordRetry(account.label, isRateLimit
|
|
2361
|
+
? "stream_rate_limit_before_commit"
|
|
2362
|
+
: "stream_error_before_commit");
|
|
2363
|
+
upstreamSpan?.end();
|
|
2364
|
+
logProxyBody({
|
|
2365
|
+
phase: "upstream_response",
|
|
2366
|
+
headers: responseHeaders,
|
|
2367
|
+
body: bodyText,
|
|
2368
|
+
bodySize: Buffer.byteLength(bodyText, "utf8"),
|
|
2369
|
+
contentType: responseHeaders["content-type"] ?? "text/event-stream",
|
|
2370
|
+
account: account.label,
|
|
2371
|
+
accountType: account.type,
|
|
2372
|
+
attempt: attemptNumber,
|
|
2373
|
+
responseStatus: response.status,
|
|
2374
|
+
durationMs: Date.now() - fetchStartMs,
|
|
2375
|
+
metadata: {
|
|
2376
|
+
logicalStatus,
|
|
2377
|
+
upstreamErrorType: preflight.errorType,
|
|
2378
|
+
},
|
|
2379
|
+
});
|
|
2380
|
+
return {
|
|
2381
|
+
retryNextAccount: true,
|
|
2382
|
+
failure: { message: preflight.message, rateLimit: isRateLimit },
|
|
2383
|
+
};
|
|
2300
2384
|
}
|
|
2301
2385
|
const streamOutcomeTracker = createStreamTerminalOutcomeTracker();
|
|
2302
2386
|
let mainStreamClosed = false;
|
|
2303
2387
|
const remainingStream = new ReadableStream({
|
|
2304
2388
|
start(controller) {
|
|
2305
|
-
|
|
2389
|
+
for (const chunk of preflight.chunks) {
|
|
2390
|
+
controller.enqueue(chunk);
|
|
2391
|
+
}
|
|
2306
2392
|
},
|
|
2307
2393
|
async pull(controller) {
|
|
2308
2394
|
if (mainStreamClosed) {
|
|
@@ -2865,10 +2951,12 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
2865
2951
|
await clearAuthCooldownAfterRefresh(account, accountState);
|
|
2866
2952
|
headers.authorization = `Bearer ${account.token}`;
|
|
2867
2953
|
const retryAttemptNumber = allocateAttemptNumber();
|
|
2954
|
+
const retryAttemptStartedAt = Date.now();
|
|
2868
2955
|
recordAttempt(account.label, account.type);
|
|
2869
2956
|
const retryLogAttempt = (status, errorType, errorMessage, extra) => logAttempt(status, errorType, errorMessage, {
|
|
2870
2957
|
...extra,
|
|
2871
2958
|
attempt: retryAttemptNumber,
|
|
2959
|
+
attemptDurationMs: Date.now() - retryAttemptStartedAt,
|
|
2872
2960
|
});
|
|
2873
2961
|
const retryBodyStr = buildUpstreamBody(account.token).bodyStr;
|
|
2874
2962
|
const retryFetchStartMs = Date.now();
|
|
@@ -3062,7 +3150,10 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
3062
3150
|
: String(retryFetchErr);
|
|
3063
3151
|
authRetryError = `network error on retry ${authRetry + 1}: ${message}`;
|
|
3064
3152
|
currentLastError = authRetryError;
|
|
3065
|
-
retryLogAttempt(502, "network_error", message, {
|
|
3153
|
+
retryLogAttempt(502, "network_error", message, {
|
|
3154
|
+
retryable: isRetryableNetworkError(retryFetchErr),
|
|
3155
|
+
errorCode: getErrorCode(retryFetchErr) ?? "unknown",
|
|
3156
|
+
});
|
|
3066
3157
|
logger.debug(`[proxy] ${authRetryError}`);
|
|
3067
3158
|
break;
|
|
3068
3159
|
}
|
|
@@ -3420,7 +3511,9 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
3420
3511
|
}
|
|
3421
3512
|
function createAnthropicAttemptLogger(args) {
|
|
3422
3513
|
const { ctx, body, toolCount, requestStart, tracer, account, attemptNumber } = args;
|
|
3514
|
+
const attemptStartedAt = Date.now();
|
|
3423
3515
|
return (status, errorType, errorMessage, extra) => {
|
|
3516
|
+
const attemptCompletedAt = Date.now();
|
|
3424
3517
|
const traceCtx = tracer?.getTraceContext();
|
|
3425
3518
|
logRequestAttempt({
|
|
3426
3519
|
timestamp: new Date().toISOString(),
|
|
@@ -3434,9 +3527,11 @@ function createAnthropicAttemptLogger(args) {
|
|
|
3434
3527
|
account: account.label,
|
|
3435
3528
|
accountType: account.type,
|
|
3436
3529
|
responseStatus: status,
|
|
3437
|
-
responseTimeMs:
|
|
3530
|
+
responseTimeMs: attemptCompletedAt - requestStart,
|
|
3531
|
+
attemptDurationMs: extra?.attemptDurationMs ?? attemptCompletedAt - attemptStartedAt,
|
|
3438
3532
|
...(errorType ? { errorType } : {}),
|
|
3439
3533
|
...(errorMessage ? { errorMessage } : {}),
|
|
3534
|
+
...(extra?.errorCode ? { errorCode: extra.errorCode } : {}),
|
|
3440
3535
|
...(extra?.inputTokens !== undefined
|
|
3441
3536
|
? { inputTokens: extra.inputTokens }
|
|
3442
3537
|
: {}),
|
|
@@ -3651,19 +3746,27 @@ async function fetchAnthropicAccountResponse(args) {
|
|
|
3651
3746
|
});
|
|
3652
3747
|
}
|
|
3653
3748
|
catch (fetchErr) {
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3749
|
+
const retryable = isRetryableNetworkError(fetchErr);
|
|
3750
|
+
// Every dispatched upstream request is an attempt, including terminal
|
|
3751
|
+
// transport failures. Record it once before preserving the throw behavior.
|
|
3657
3752
|
sawNetworkError = true;
|
|
3658
3753
|
recordAttemptError(account.label, account.type, 502);
|
|
3659
3754
|
const errorCode = getErrorCode(fetchErr) ?? "unknown";
|
|
3660
3755
|
const errorMessage = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
3661
3756
|
lastError = errorMessage;
|
|
3662
|
-
logger.always(`[proxy] fetch error account=${account.label} code=${errorCode} (retryable): ${errorMessage}`);
|
|
3663
|
-
logAttempt(502, "network_error", errorMessage
|
|
3757
|
+
logger.always(`[proxy] fetch error account=${account.label} code=${errorCode} (${retryable ? "retryable" : "terminal"}): ${errorMessage}`);
|
|
3758
|
+
logAttempt(502, "network_error", errorMessage, {
|
|
3759
|
+
retryable,
|
|
3760
|
+
errorCode,
|
|
3761
|
+
});
|
|
3664
3762
|
tracer?.setError("network_error", errorMessage);
|
|
3665
|
-
|
|
3763
|
+
if (retryable) {
|
|
3764
|
+
tracer?.recordRetry(account.label, "network_error");
|
|
3765
|
+
}
|
|
3666
3766
|
currentUpstreamSpan?.end();
|
|
3767
|
+
if (!retryable) {
|
|
3768
|
+
throw fetchErr;
|
|
3769
|
+
}
|
|
3667
3770
|
return {
|
|
3668
3771
|
continueLoop: true,
|
|
3669
3772
|
retrySameAccount: true,
|
|
@@ -4083,10 +4186,16 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
4083
4186
|
attemptNumber: loopState.attemptNumber,
|
|
4084
4187
|
finalBodyStr: preparedAttempt.finalBodyStr,
|
|
4085
4188
|
upstreamSpan,
|
|
4189
|
+
logAttempt,
|
|
4086
4190
|
logProxyBody,
|
|
4087
4191
|
logFinalRequest,
|
|
4088
4192
|
});
|
|
4089
4193
|
if ("retryNextAccount" in successResult) {
|
|
4194
|
+
if (successResult.failure) {
|
|
4195
|
+
loopState.lastError = successResult.failure.message;
|
|
4196
|
+
loopState.sawRateLimit ||= successResult.failure.rateLimit;
|
|
4197
|
+
loopState.sawTransientFailure ||= !successResult.failure.rateLimit;
|
|
4198
|
+
}
|
|
4090
4199
|
continue accountLoop;
|
|
4091
4200
|
}
|
|
4092
4201
|
return successResult.response;
|
|
@@ -4469,16 +4578,13 @@ function describeTransportError(error) {
|
|
|
4469
4578
|
*/
|
|
4470
4579
|
function isRetryableNetworkError(error) {
|
|
4471
4580
|
const code = getErrorCode(error);
|
|
4472
|
-
// Check non-retryable codes FIRST — before the string-based heuristic
|
|
4473
|
-
// which could false-positive on error messages containing these strings.
|
|
4474
|
-
const NON_RETRYABLE_CODES = ["ENOTFOUND"];
|
|
4475
|
-
if (code && NON_RETRYABLE_CODES.includes(code)) {
|
|
4476
|
-
return false;
|
|
4477
|
-
}
|
|
4478
4581
|
if (code &&
|
|
4479
4582
|
[
|
|
4480
4583
|
"ECONNREFUSED",
|
|
4481
4584
|
"ECONNRESET",
|
|
4585
|
+
// The Anthropic host is fixed, so ENOTFOUND can be a transient resolver
|
|
4586
|
+
// outage. Keep it inside the existing bounded same-account retry budget.
|
|
4587
|
+
"ENOTFOUND",
|
|
4482
4588
|
"ETIMEDOUT",
|
|
4483
4589
|
"EHOSTUNREACH",
|
|
4484
4590
|
"UND_ERR_CONNECT_TIMEOUT",
|
|
@@ -4490,13 +4596,9 @@ function isRetryableNetworkError(error) {
|
|
|
4490
4596
|
}
|
|
4491
4597
|
const message = error instanceof Error ? error.message : String(error);
|
|
4492
4598
|
const normalized = message.toLowerCase();
|
|
4493
|
-
// Exclude ENOTFOUND from string-based heuristic — DNS failures are permanent
|
|
4494
|
-
// and rotating accounts won't help since they all hit the same host.
|
|
4495
|
-
if (normalized.includes("enotfound")) {
|
|
4496
|
-
return false;
|
|
4497
|
-
}
|
|
4498
4599
|
return (normalized.includes("econnrefused") ||
|
|
4499
4600
|
normalized.includes("econnreset") ||
|
|
4601
|
+
normalized.includes("enotfound") ||
|
|
4500
4602
|
normalized.includes("etimedout") ||
|
|
4501
4603
|
normalized.includes("timed out") ||
|
|
4502
4604
|
normalized.includes("connection error") ||
|
package/dist/lib/types/cli.d.ts
CHANGED
|
@@ -791,6 +791,13 @@ export type ProxyStatusArgs = {
|
|
|
791
791
|
format?: "text" | "json";
|
|
792
792
|
quiet?: boolean;
|
|
793
793
|
};
|
|
794
|
+
/** Arguments accepted by `neurolink proxy analyze` */
|
|
795
|
+
export type ProxyAnalyzeArgs = {
|
|
796
|
+
logsDir?: string;
|
|
797
|
+
since?: string;
|
|
798
|
+
format?: "text" | "json";
|
|
799
|
+
quiet?: boolean;
|
|
800
|
+
};
|
|
794
801
|
/** Arguments accepted by hidden `neurolink proxy guard` command */
|
|
795
802
|
export type ProxyGuardArgs = {
|
|
796
803
|
host?: string;
|
|
@@ -437,6 +437,8 @@ export type RequestLogEntry = {
|
|
|
437
437
|
responseTimeMs: number;
|
|
438
438
|
errorType?: string;
|
|
439
439
|
errorMessage?: string;
|
|
440
|
+
/** Low-level transport code such as ETIMEDOUT or EADDRNOTAVAIL. */
|
|
441
|
+
errorCode?: string;
|
|
440
442
|
inputTokens?: number;
|
|
441
443
|
outputTokens?: number;
|
|
442
444
|
cacheCreationTokens?: number;
|
|
@@ -458,9 +460,14 @@ export type RequestAttemptLogEntry = {
|
|
|
458
460
|
account: string;
|
|
459
461
|
accountType: string;
|
|
460
462
|
responseStatus: number;
|
|
463
|
+
/** End-to-end request age when this attempt completed. */
|
|
461
464
|
responseTimeMs: number;
|
|
465
|
+
/** Time spent in this specific account attempt. */
|
|
466
|
+
attemptDurationMs?: number;
|
|
462
467
|
errorType?: string;
|
|
463
468
|
errorMessage?: string;
|
|
469
|
+
/** Low-level transport code such as ETIMEDOUT or EADDRNOTAVAIL. */
|
|
470
|
+
errorCode?: string;
|
|
464
471
|
/** Whether this failed attempt may be retried without changing the request. */
|
|
465
472
|
retryable?: boolean;
|
|
466
473
|
/** Distinguishes short-lived admission throttles from exhausted quota windows. */
|
|
@@ -514,8 +521,12 @@ export type AnthropicAttemptLogger = (status: number, errorType?: string, errorM
|
|
|
514
521
|
cacheCreationTokens?: number;
|
|
515
522
|
cacheReadTokens?: number;
|
|
516
523
|
retryable?: boolean;
|
|
524
|
+
/** Low-level transport code such as ETIMEDOUT or EADDRNOTAVAIL. */
|
|
525
|
+
errorCode?: string;
|
|
517
526
|
rateLimitKind?: "transient" | "quota";
|
|
518
527
|
cooldownReason?: "transient" | "session" | "weekly" | "unified";
|
|
528
|
+
/** Override for nested retries whose attempt starts after this logger. */
|
|
529
|
+
attemptDurationMs?: number;
|
|
519
530
|
/** Override used when one account selection performs an OAuth retry fetch. */
|
|
520
531
|
attempt?: number;
|
|
521
532
|
}) => void;
|
|
@@ -552,9 +563,40 @@ export type LoadedClaudeAccountContext = {
|
|
|
552
563
|
};
|
|
553
564
|
export type AnthropicSuccessResult = {
|
|
554
565
|
retryNextAccount: true;
|
|
566
|
+
failure?: {
|
|
567
|
+
message: string;
|
|
568
|
+
rateLimit: boolean;
|
|
569
|
+
};
|
|
555
570
|
} | {
|
|
556
571
|
response: Response | unknown;
|
|
557
572
|
};
|
|
573
|
+
/** Result of buffering only enough upstream SSE to make a retry-safe decision. */
|
|
574
|
+
export type AnthropicStreamPreflightResult = {
|
|
575
|
+
kind: "ready";
|
|
576
|
+
chunks: Uint8Array[];
|
|
577
|
+
} | {
|
|
578
|
+
kind: "empty";
|
|
579
|
+
chunks: Uint8Array[];
|
|
580
|
+
} | {
|
|
581
|
+
kind: "transport_error";
|
|
582
|
+
chunks: Uint8Array[];
|
|
583
|
+
error: unknown;
|
|
584
|
+
} | {
|
|
585
|
+
kind: "sse_error";
|
|
586
|
+
chunks: Uint8Array[];
|
|
587
|
+
errorType: string;
|
|
588
|
+
message: string;
|
|
589
|
+
};
|
|
590
|
+
/** One complete Server-Sent Event extracted from an incremental buffer. */
|
|
591
|
+
export type ParsedSSEEvent = {
|
|
592
|
+
event: string;
|
|
593
|
+
data: string;
|
|
594
|
+
};
|
|
595
|
+
/** Complete SSE events plus the trailing partial frame. */
|
|
596
|
+
export type ParsedSSEBuffer = {
|
|
597
|
+
events: ParsedSSEEvent[];
|
|
598
|
+
remainder: string;
|
|
599
|
+
};
|
|
558
600
|
export type AnthropicAuthRetryResult = {
|
|
559
601
|
response?: Response | unknown;
|
|
560
602
|
continueLoop: boolean;
|
|
@@ -804,6 +846,8 @@ export type ProxyReadinessState = {
|
|
|
804
846
|
startTimeMs: number;
|
|
805
847
|
acceptingConnections: boolean;
|
|
806
848
|
ready: boolean;
|
|
849
|
+
/** True only while the updater is draining inference traffic. */
|
|
850
|
+
drainingForUpdate: boolean;
|
|
807
851
|
readyAtMs?: number;
|
|
808
852
|
};
|
|
809
853
|
/** Structured response returned by the proxy /health endpoint. */
|
|
@@ -811,6 +855,7 @@ export type ProxyHealthResponse = {
|
|
|
811
855
|
status: "ok" | "starting";
|
|
812
856
|
ready: boolean;
|
|
813
857
|
acceptingConnections: boolean;
|
|
858
|
+
drainingForUpdate: boolean;
|
|
814
859
|
strategy: string;
|
|
815
860
|
passthrough: boolean;
|
|
816
861
|
version: string;
|
|
@@ -945,6 +990,198 @@ export type ProxyRuntimeActivity = {
|
|
|
945
990
|
activeRequests: number;
|
|
946
991
|
lastActivityAt: string | null;
|
|
947
992
|
};
|
|
993
|
+
/** Terminal state observed while the HTTP adapter relays a response body. */
|
|
994
|
+
export type ProxyResponseTerminalOutcome = "completed" | "bodyless" | "client_cancelled" | "stream_error";
|
|
995
|
+
/** Non-blocking callbacks for response lifecycle metadata. */
|
|
996
|
+
export type ProxyResponseTrackingObserver = {
|
|
997
|
+
onFirstChunk?: (details: {
|
|
998
|
+
/** Decoded response-body bytes observed by the adapter. */
|
|
999
|
+
observedBodyBytes: number;
|
|
1000
|
+
responseChunks: 1;
|
|
1001
|
+
}) => void;
|
|
1002
|
+
onTerminal?: (details: {
|
|
1003
|
+
outcome: ProxyResponseTerminalOutcome;
|
|
1004
|
+
/** Decoded response-body bytes observed by the adapter. */
|
|
1005
|
+
observedBodyBytes: number;
|
|
1006
|
+
responseChunks: number;
|
|
1007
|
+
}) => void;
|
|
1008
|
+
};
|
|
1009
|
+
/** Versioned lifecycle event names persisted by the proxy adapter. */
|
|
1010
|
+
export type ProxyLifecycleEventName = "request_accepted" | "response_headers" | "response_first_chunk" | "request_terminal";
|
|
1011
|
+
/** Client-facing terminal classifications recorded by lifecycle metadata. */
|
|
1012
|
+
export type ProxyLifecycleTerminalOutcome = ProxyResponseTerminalOutcome | "handler_error";
|
|
1013
|
+
/** Content-free lifecycle event accepted by the bounded metadata logger. */
|
|
1014
|
+
export type ProxyLifecycleEventInput = {
|
|
1015
|
+
event: ProxyLifecycleEventName;
|
|
1016
|
+
requestId: string;
|
|
1017
|
+
method: string;
|
|
1018
|
+
path: string;
|
|
1019
|
+
model?: string;
|
|
1020
|
+
stream?: boolean;
|
|
1021
|
+
toolCount?: number;
|
|
1022
|
+
sessionHash?: string;
|
|
1023
|
+
requestBytes?: number;
|
|
1024
|
+
responseStatus?: number;
|
|
1025
|
+
/** Decoded response-body bytes observed by the adapter. */
|
|
1026
|
+
observedBodyBytes?: number;
|
|
1027
|
+
responseChunks?: number;
|
|
1028
|
+
elapsedMs?: number;
|
|
1029
|
+
terminalOutcome?: ProxyLifecycleTerminalOutcome;
|
|
1030
|
+
errorType?: string;
|
|
1031
|
+
errorCode?: string;
|
|
1032
|
+
timestampMs?: number;
|
|
1033
|
+
monotonicMs?: number;
|
|
1034
|
+
};
|
|
1035
|
+
/** Data-quality counters for the bounded lifecycle metadata sink. */
|
|
1036
|
+
export type ProxyLifecycleLoggerSnapshot = {
|
|
1037
|
+
enabled: boolean;
|
|
1038
|
+
schemaVersion: number;
|
|
1039
|
+
processInstanceId: string;
|
|
1040
|
+
nextSequence: number;
|
|
1041
|
+
attempted: number;
|
|
1042
|
+
enqueued: number;
|
|
1043
|
+
written: number;
|
|
1044
|
+
dropped: number;
|
|
1045
|
+
queueDrops: number;
|
|
1046
|
+
invalidDrops: number;
|
|
1047
|
+
writeDrops: number;
|
|
1048
|
+
writeFailures: number;
|
|
1049
|
+
pending: number;
|
|
1050
|
+
inFlight: number;
|
|
1051
|
+
flushing: boolean;
|
|
1052
|
+
};
|
|
1053
|
+
/** Lifecycle logger configuration. Queue overrides are used by stress tests. */
|
|
1054
|
+
export type ProxyLifecycleLoggerOptions = {
|
|
1055
|
+
enabled: boolean;
|
|
1056
|
+
logDir?: string;
|
|
1057
|
+
queueCapacity?: number;
|
|
1058
|
+
batchSize?: number;
|
|
1059
|
+
flushIntervalMs?: number;
|
|
1060
|
+
};
|
|
1061
|
+
/** Serialized lifecycle line awaiting a bounded batch write. */
|
|
1062
|
+
export type QueuedProxyLifecycleEvent = {
|
|
1063
|
+
logDir: string;
|
|
1064
|
+
date: string;
|
|
1065
|
+
record: Record<string, unknown>;
|
|
1066
|
+
};
|
|
1067
|
+
/** Percentile summary used by offline proxy log analysis. */
|
|
1068
|
+
export type ProxyLatencySummary = {
|
|
1069
|
+
count: number;
|
|
1070
|
+
p50: number | null;
|
|
1071
|
+
p95: number | null;
|
|
1072
|
+
p99: number | null;
|
|
1073
|
+
max: number | null;
|
|
1074
|
+
};
|
|
1075
|
+
/** Per-account request and rate-limit totals reconstructed from JSONL logs. */
|
|
1076
|
+
export type ProxyAnalysisAccount = {
|
|
1077
|
+
account: string;
|
|
1078
|
+
accountType: string;
|
|
1079
|
+
attempts: number;
|
|
1080
|
+
attemptErrors: number;
|
|
1081
|
+
finalRequests: number;
|
|
1082
|
+
finalErrors: number;
|
|
1083
|
+
transientRateLimits: number;
|
|
1084
|
+
quotaRateLimits: number;
|
|
1085
|
+
unclassifiedRateLimits: number;
|
|
1086
|
+
};
|
|
1087
|
+
/** Offline report generated from proxy request, attempt, and lifecycle logs. */
|
|
1088
|
+
export type ProxyAnalysisReport = {
|
|
1089
|
+
generatedAt: string;
|
|
1090
|
+
since: string;
|
|
1091
|
+
logsDir: string;
|
|
1092
|
+
files: {
|
|
1093
|
+
lifecycle: number;
|
|
1094
|
+
requests: number;
|
|
1095
|
+
attempts: number;
|
|
1096
|
+
};
|
|
1097
|
+
coverage: {
|
|
1098
|
+
lifecycle: boolean;
|
|
1099
|
+
finalRequests: boolean;
|
|
1100
|
+
attempts: boolean;
|
|
1101
|
+
attemptLatency: boolean;
|
|
1102
|
+
cacheUsage: boolean;
|
|
1103
|
+
};
|
|
1104
|
+
dataQuality: {
|
|
1105
|
+
linesRead: number;
|
|
1106
|
+
malformedLines: number;
|
|
1107
|
+
unsupportedLifecycleLines: number;
|
|
1108
|
+
lifecycleSequenceGaps: number;
|
|
1109
|
+
lifecycleSequenceDuplicates: number;
|
|
1110
|
+
};
|
|
1111
|
+
lifecycle: {
|
|
1112
|
+
accepted: number;
|
|
1113
|
+
headers: number;
|
|
1114
|
+
firstChunks: number;
|
|
1115
|
+
terminal: number;
|
|
1116
|
+
unsettled: number;
|
|
1117
|
+
terminalOutcomes: Record<string, number>;
|
|
1118
|
+
errorTypes: Record<string, number>;
|
|
1119
|
+
errorCodes: Record<string, number>;
|
|
1120
|
+
};
|
|
1121
|
+
requests: {
|
|
1122
|
+
completed: number;
|
|
1123
|
+
success: number;
|
|
1124
|
+
errors: number;
|
|
1125
|
+
finalRateLimits: number;
|
|
1126
|
+
recoveredAfterRetry: number;
|
|
1127
|
+
errorTypes: Record<string, number>;
|
|
1128
|
+
errorCodes: Record<string, number>;
|
|
1129
|
+
};
|
|
1130
|
+
attempts: {
|
|
1131
|
+
total: number;
|
|
1132
|
+
errors: number;
|
|
1133
|
+
errorTypes: Record<string, number>;
|
|
1134
|
+
errorCodes: Record<string, number>;
|
|
1135
|
+
};
|
|
1136
|
+
rateLimits: {
|
|
1137
|
+
attemptRateLimits: number;
|
|
1138
|
+
transient: number;
|
|
1139
|
+
quota: number;
|
|
1140
|
+
unclassified: number;
|
|
1141
|
+
};
|
|
1142
|
+
latencyMs: {
|
|
1143
|
+
headers: ProxyLatencySummary;
|
|
1144
|
+
firstChunk: ProxyLatencySummary;
|
|
1145
|
+
terminal: ProxyLatencySummary;
|
|
1146
|
+
finalRequest: ProxyLatencySummary;
|
|
1147
|
+
attempt: ProxyLatencySummary;
|
|
1148
|
+
singleAttemptDelta: ProxyLatencySummary;
|
|
1149
|
+
};
|
|
1150
|
+
cache: {
|
|
1151
|
+
requestsWithUsage: number;
|
|
1152
|
+
requestsWithCacheRead: number;
|
|
1153
|
+
cacheReadTokens: number;
|
|
1154
|
+
cacheCreationTokens: number;
|
|
1155
|
+
inputTokens: number;
|
|
1156
|
+
requestHitRate: number | null;
|
|
1157
|
+
};
|
|
1158
|
+
accounts: ProxyAnalysisAccount[];
|
|
1159
|
+
};
|
|
1160
|
+
/** Inputs for the offline proxy JSONL analyzer. */
|
|
1161
|
+
export type ProxyAnalysisOptions = {
|
|
1162
|
+
logsDir?: string;
|
|
1163
|
+
since?: string;
|
|
1164
|
+
nowMs?: number;
|
|
1165
|
+
};
|
|
1166
|
+
/** Attempt timing retained while joining offline proxy log records. */
|
|
1167
|
+
export type ProxyAnalysisAttemptRecord = {
|
|
1168
|
+
count: number;
|
|
1169
|
+
hadError: boolean;
|
|
1170
|
+
totalDurationMs: number;
|
|
1171
|
+
durationCount: number;
|
|
1172
|
+
};
|
|
1173
|
+
/** Final request fields retained while joining offline proxy log records. */
|
|
1174
|
+
export type ProxyAnalysisFinalRequestRecord = {
|
|
1175
|
+
status: number;
|
|
1176
|
+
durationMs: number | null;
|
|
1177
|
+
account: string;
|
|
1178
|
+
accountType: string;
|
|
1179
|
+
inputTokens: number | null;
|
|
1180
|
+
cacheReadTokens: number | null;
|
|
1181
|
+
cacheCreationTokens: number | null;
|
|
1182
|
+
errorType: string | null;
|
|
1183
|
+
errorCode: string | null;
|
|
1184
|
+
};
|
|
948
1185
|
/** Request metadata retained by the HTTP adapter for terminal error logging. */
|
|
949
1186
|
export type RuntimeRequestMetadata = {
|
|
950
1187
|
requestId: string;
|
|
@@ -954,6 +1191,10 @@ export type RuntimeRequestMetadata = {
|
|
|
954
1191
|
model: string;
|
|
955
1192
|
stream: boolean;
|
|
956
1193
|
toolCount: number;
|
|
1194
|
+
/** Admission decision captured before an updater drain can race the route. */
|
|
1195
|
+
rejectForUpdate?: boolean;
|
|
1196
|
+
terminalErrorType?: string;
|
|
1197
|
+
terminalErrorCode?: string;
|
|
957
1198
|
};
|
|
958
1199
|
/** Accumulated upstream body capture from a raw stream. */
|
|
959
1200
|
export type RawStreamCapture = {
|
|
@@ -1119,6 +1360,14 @@ export type UpdateState = {
|
|
|
1119
1360
|
lastUpdateVersion: string | null;
|
|
1120
1361
|
/** Installed by the updater but not yet confirmed as the running version. */
|
|
1121
1362
|
pendingRestartVersion: string | null;
|
|
1363
|
+
/** Why an available update has not yet reached a safe install/restart boundary. */
|
|
1364
|
+
deferredUpdate: {
|
|
1365
|
+
version: string;
|
|
1366
|
+
since: string;
|
|
1367
|
+
updatedAt: string;
|
|
1368
|
+
reason: "waiting_for_quiet" | "draining" | "drain_timeout" | "drain_unavailable" | "activity_unavailable";
|
|
1369
|
+
activeRequests: number | null;
|
|
1370
|
+
} | null;
|
|
1122
1371
|
/** Last updater failure, retained until a successful update or replacement. */
|
|
1123
1372
|
lastFailure: {
|
|
1124
1373
|
at: string;
|
|
@@ -1127,6 +1376,26 @@ export type UpdateState = {
|
|
|
1127
1376
|
message: string;
|
|
1128
1377
|
} | null;
|
|
1129
1378
|
};
|
|
1379
|
+
/** Result from waiting for a non-disruptive updater execution window. */
|
|
1380
|
+
export type ProxyUpdateWindowResult = {
|
|
1381
|
+
ready: boolean;
|
|
1382
|
+
draining: boolean;
|
|
1383
|
+
reason?: "stopping" | "parent_stopped" | "drain_failed" | "drain_timeout";
|
|
1384
|
+
};
|
|
1385
|
+
/** Dependencies and timing controls for the updater's safe-window coordinator. */
|
|
1386
|
+
export type ProxyUpdateWindowOptions = {
|
|
1387
|
+
quietThresholdMs: number;
|
|
1388
|
+
quietWaitMs: number;
|
|
1389
|
+
drainWaitMs: number;
|
|
1390
|
+
pollIntervalMs: number;
|
|
1391
|
+
getActivity: () => Promise<ProxyRuntimeActivity | null>;
|
|
1392
|
+
setDraining: (draining: boolean) => Promise<boolean>;
|
|
1393
|
+
isStopping: () => boolean;
|
|
1394
|
+
isParentAlive: () => boolean;
|
|
1395
|
+
onPhase?: (phase: "waiting_for_quiet" | "draining" | "drain_timeout", activity: ProxyRuntimeActivity | null) => void;
|
|
1396
|
+
now?: () => number;
|
|
1397
|
+
sleep?: (ms: number) => Promise<void>;
|
|
1398
|
+
};
|
|
1130
1399
|
/** Result of validating a newly installed CLI through the stable trampoline. */
|
|
1131
1400
|
export type InstalledVersionValidation = {
|
|
1132
1401
|
version?: string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { ProxyActivitySnapshot } from "../types/index.js";
|
|
1
|
+
import type { ProxyActivitySnapshot, ProxyResponseTrackingObserver } from "../types/index.js";
|
|
2
2
|
/** Track one client-facing proxy request until its response body settles. */
|
|
3
3
|
export declare function beginProxyRequest(): () => void;
|
|
4
4
|
export declare function getProxyActivitySnapshot(): ProxyActivitySnapshot;
|
|
5
5
|
export declare function isProxyActivityQuiet(snapshot: ProxyActivitySnapshot, quietThresholdMs: number, nowMs?: number): boolean;
|
|
6
6
|
/** Keep activity open until the response body completes, errors, or is cancelled. */
|
|
7
|
-
export declare function trackProxyResponse(response: Response, finishRequest: () => void): Response;
|
|
7
|
+
export declare function trackProxyResponse(response: Response, finishRequest: () => void, observer?: ProxyResponseTrackingObserver): Response;
|
|
8
8
|
export declare function resetProxyActivityForTests(): void;
|