@juspay/neurolink 9.93.0 → 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 +6 -0
- package/dist/browser/neurolink.min.js +379 -382
- package/dist/cli/commands/proxy.d.ts +3 -2
- package/dist/cli/commands/proxy.js +209 -48
- 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/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/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 +114 -17
- package/dist/lib/types/cli.d.ts +7 -0
- package/dist/lib/types/proxy.d.ts +187 -0
- 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/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 +114 -17
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/proxy.d.ts +187 -0
- package/package.json +4 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function isQuiet(activity, quietThresholdMs, nowMs) {
|
|
2
|
+
if (activity.activeRequests > 0) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
if (!activity.lastActivityAt) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
const lastActivityMs = Date.parse(activity.lastActivityAt);
|
|
9
|
+
return (Number.isFinite(lastActivityMs) &&
|
|
10
|
+
nowMs - lastActivityMs >= quietThresholdMs);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Prefer a naturally quiet window, then briefly drain new inference traffic.
|
|
14
|
+
* Existing requests are never interrupted; a bounded drain failure is returned
|
|
15
|
+
* to the caller so admission can be reopened and retried later.
|
|
16
|
+
*/
|
|
17
|
+
export async function waitForProxyUpdateWindow(options) {
|
|
18
|
+
const now = options.now ?? Date.now;
|
|
19
|
+
const wait = options.sleep ??
|
|
20
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
21
|
+
const quietDeadline = now() + options.quietWaitMs;
|
|
22
|
+
while (now() < quietDeadline) {
|
|
23
|
+
if (options.isStopping()) {
|
|
24
|
+
return { ready: false, draining: false, reason: "stopping" };
|
|
25
|
+
}
|
|
26
|
+
if (!options.isParentAlive()) {
|
|
27
|
+
return { ready: false, draining: false, reason: "parent_stopped" };
|
|
28
|
+
}
|
|
29
|
+
const activity = await options.getActivity();
|
|
30
|
+
if (activity && isQuiet(activity, options.quietThresholdMs, now())) {
|
|
31
|
+
return { ready: true, draining: false };
|
|
32
|
+
}
|
|
33
|
+
options.onPhase?.("waiting_for_quiet", activity);
|
|
34
|
+
await wait(options.pollIntervalMs);
|
|
35
|
+
}
|
|
36
|
+
if (!(await options.setDraining(true))) {
|
|
37
|
+
// The control response may have been lost after the parent applied it.
|
|
38
|
+
// Treat state as unknown/draining so the caller always attempts resume.
|
|
39
|
+
return { ready: false, draining: true, reason: "drain_failed" };
|
|
40
|
+
}
|
|
41
|
+
const drainDeadline = now() + options.drainWaitMs;
|
|
42
|
+
while (now() < drainDeadline) {
|
|
43
|
+
if (options.isStopping()) {
|
|
44
|
+
return { ready: false, draining: true, reason: "stopping" };
|
|
45
|
+
}
|
|
46
|
+
if (!options.isParentAlive()) {
|
|
47
|
+
return { ready: false, draining: true, reason: "parent_stopped" };
|
|
48
|
+
}
|
|
49
|
+
const activity = await options.getActivity();
|
|
50
|
+
options.onPhase?.("draining", activity);
|
|
51
|
+
if (activity?.activeRequests === 0) {
|
|
52
|
+
return { ready: true, draining: true };
|
|
53
|
+
}
|
|
54
|
+
await wait(options.pollIntervalMs);
|
|
55
|
+
}
|
|
56
|
+
const activity = await options.getActivity();
|
|
57
|
+
options.onPhase?.("drain_timeout", activity);
|
|
58
|
+
return { ready: false, draining: true, reason: "drain_timeout" };
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=updateCoordinator.js.map
|
|
@@ -54,6 +54,10 @@ export declare function recordUpdateInstalled(version: string, stateFilePath?: s
|
|
|
54
54
|
export declare function abandonPendingUpdate(version: string, stateFilePath?: string): boolean;
|
|
55
55
|
/** Persist a stage-specific updater failure so status remains actionable. */
|
|
56
56
|
export declare function recordUpdateFailure(version: string, stage: NonNullable<UpdateState["lastFailure"]>["stage"], message: string, stateFilePath?: string): void;
|
|
57
|
+
/** Persist why a discovered update is waiting without treating it as a failure. */
|
|
58
|
+
export declare function recordUpdateDeferred(version: string, reason: NonNullable<UpdateState["deferredUpdate"]>["reason"], activeRequests: number | null, stateFilePath?: string): void;
|
|
59
|
+
/** Clear matching updater deferral metadata once work can proceed. */
|
|
60
|
+
export declare function clearUpdateDeferral(version?: string, stateFilePath?: string): boolean;
|
|
57
61
|
/** Complete an updater-managed install after a manual or automatic restart. */
|
|
58
62
|
export declare function reconcileRunningUpdate(runningVersion: string, stateFilePath?: string): boolean;
|
|
59
63
|
/**
|
|
@@ -56,6 +56,26 @@ function isValidLastFailure(value) {
|
|
|
56
56
|
UPDATE_FAILURE_STAGES.has(candidate.stage) &&
|
|
57
57
|
typeof candidate.message === "string");
|
|
58
58
|
}
|
|
59
|
+
function isValidDeferredUpdate(value) {
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const candidate = value;
|
|
64
|
+
return (typeof candidate.version === "string" &&
|
|
65
|
+
typeof candidate.since === "string" &&
|
|
66
|
+
typeof candidate.updatedAt === "string" &&
|
|
67
|
+
[
|
|
68
|
+
"waiting_for_quiet",
|
|
69
|
+
"draining",
|
|
70
|
+
"drain_timeout",
|
|
71
|
+
"drain_unavailable",
|
|
72
|
+
"activity_unavailable",
|
|
73
|
+
].includes(String(candidate.reason)) &&
|
|
74
|
+
(candidate.activeRequests === null ||
|
|
75
|
+
(typeof candidate.activeRequests === "number" &&
|
|
76
|
+
Number.isInteger(candidate.activeRequests) &&
|
|
77
|
+
candidate.activeRequests >= 0)));
|
|
78
|
+
}
|
|
59
79
|
// ============================================
|
|
60
80
|
// Exported Functions
|
|
61
81
|
// ============================================
|
|
@@ -70,6 +90,7 @@ export function getDefaultUpdateState() {
|
|
|
70
90
|
lastUpdateAt: null,
|
|
71
91
|
lastUpdateVersion: null,
|
|
72
92
|
pendingRestartVersion: null,
|
|
93
|
+
deferredUpdate: null,
|
|
73
94
|
lastFailure: null,
|
|
74
95
|
};
|
|
75
96
|
}
|
|
@@ -106,6 +127,9 @@ export function loadUpdateState(stateFilePath) {
|
|
|
106
127
|
pendingRestartVersion: typeof candidate.pendingRestartVersion === "string"
|
|
107
128
|
? candidate.pendingRestartVersion
|
|
108
129
|
: null,
|
|
130
|
+
deferredUpdate: isValidDeferredUpdate(candidate.deferredUpdate)
|
|
131
|
+
? candidate.deferredUpdate
|
|
132
|
+
: null,
|
|
109
133
|
lastFailure: isValidLastFailure(candidate.lastFailure)
|
|
110
134
|
? candidate.lastFailure
|
|
111
135
|
: null,
|
|
@@ -185,6 +209,7 @@ export function recordSuccessfulUpdate(version, stateFilePath) {
|
|
|
185
209
|
state.lastUpdateAt = new Date().toISOString();
|
|
186
210
|
state.lastUpdateVersion = version;
|
|
187
211
|
state.pendingRestartVersion = null;
|
|
212
|
+
state.deferredUpdate = null;
|
|
188
213
|
state.lastFailure = null;
|
|
189
214
|
delete state.suppressedVersions[version];
|
|
190
215
|
saveUpdateState(state, stateFilePath);
|
|
@@ -217,6 +242,32 @@ export function recordUpdateFailure(version, stage, message, stateFilePath) {
|
|
|
217
242
|
};
|
|
218
243
|
saveUpdateState(state, stateFilePath);
|
|
219
244
|
}
|
|
245
|
+
/** Persist why a discovered update is waiting without treating it as a failure. */
|
|
246
|
+
export function recordUpdateDeferred(version, reason, activeRequests, stateFilePath) {
|
|
247
|
+
const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
|
|
248
|
+
const now = new Date().toISOString();
|
|
249
|
+
state.deferredUpdate = {
|
|
250
|
+
version,
|
|
251
|
+
since: state.deferredUpdate?.version === version
|
|
252
|
+
? state.deferredUpdate.since
|
|
253
|
+
: now,
|
|
254
|
+
updatedAt: now,
|
|
255
|
+
reason,
|
|
256
|
+
activeRequests,
|
|
257
|
+
};
|
|
258
|
+
saveUpdateState(state, stateFilePath);
|
|
259
|
+
}
|
|
260
|
+
/** Clear matching updater deferral metadata once work can proceed. */
|
|
261
|
+
export function clearUpdateDeferral(version, stateFilePath) {
|
|
262
|
+
const state = loadUpdateState(stateFilePath);
|
|
263
|
+
if (!state?.deferredUpdate ||
|
|
264
|
+
(version !== undefined && state.deferredUpdate.version !== version)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
state.deferredUpdate = null;
|
|
268
|
+
saveUpdateState(state, stateFilePath);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
220
271
|
/** Complete an updater-managed install after a manual or automatic restart. */
|
|
221
272
|
export function reconcileRunningUpdate(runningVersion, stateFilePath) {
|
|
222
273
|
const state = loadUpdateState(stateFilePath);
|
|
@@ -143,6 +143,7 @@ declare function handleAnthropicStreamingSuccessResponse(args: {
|
|
|
143
143
|
attemptNumber: number;
|
|
144
144
|
finalBodyStr: string;
|
|
145
145
|
upstreamSpan?: import("@opentelemetry/api").Span;
|
|
146
|
+
logAttempt: AnthropicAttemptLogger;
|
|
146
147
|
logProxyBody: ProxyBodyCaptureLogger;
|
|
147
148
|
logFinalRequest: (status: number, accountLabel: string, accountType: string, errorType?: string, errorMessage?: string, extra?: {
|
|
148
149
|
inputTokens?: number;
|
|
@@ -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();
|
|
@@ -3423,7 +3511,9 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
3423
3511
|
}
|
|
3424
3512
|
function createAnthropicAttemptLogger(args) {
|
|
3425
3513
|
const { ctx, body, toolCount, requestStart, tracer, account, attemptNumber } = args;
|
|
3514
|
+
const attemptStartedAt = Date.now();
|
|
3426
3515
|
return (status, errorType, errorMessage, extra) => {
|
|
3516
|
+
const attemptCompletedAt = Date.now();
|
|
3427
3517
|
const traceCtx = tracer?.getTraceContext();
|
|
3428
3518
|
logRequestAttempt({
|
|
3429
3519
|
timestamp: new Date().toISOString(),
|
|
@@ -3437,7 +3527,8 @@ function createAnthropicAttemptLogger(args) {
|
|
|
3437
3527
|
account: account.label,
|
|
3438
3528
|
accountType: account.type,
|
|
3439
3529
|
responseStatus: status,
|
|
3440
|
-
responseTimeMs:
|
|
3530
|
+
responseTimeMs: attemptCompletedAt - requestStart,
|
|
3531
|
+
attemptDurationMs: extra?.attemptDurationMs ?? attemptCompletedAt - attemptStartedAt,
|
|
3441
3532
|
...(errorType ? { errorType } : {}),
|
|
3442
3533
|
...(errorMessage ? { errorMessage } : {}),
|
|
3443
3534
|
...(extra?.errorCode ? { errorCode: extra.errorCode } : {}),
|
|
@@ -4095,10 +4186,16 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
4095
4186
|
attemptNumber: loopState.attemptNumber,
|
|
4096
4187
|
finalBodyStr: preparedAttempt.finalBodyStr,
|
|
4097
4188
|
upstreamSpan,
|
|
4189
|
+
logAttempt,
|
|
4098
4190
|
logProxyBody,
|
|
4099
4191
|
logFinalRequest,
|
|
4100
4192
|
});
|
|
4101
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
|
+
}
|
|
4102
4199
|
continue accountLoop;
|
|
4103
4200
|
}
|
|
4104
4201
|
return successResult.response;
|
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;
|
|
@@ -460,7 +460,10 @@ export type RequestAttemptLogEntry = {
|
|
|
460
460
|
account: string;
|
|
461
461
|
accountType: string;
|
|
462
462
|
responseStatus: number;
|
|
463
|
+
/** End-to-end request age when this attempt completed. */
|
|
463
464
|
responseTimeMs: number;
|
|
465
|
+
/** Time spent in this specific account attempt. */
|
|
466
|
+
attemptDurationMs?: number;
|
|
464
467
|
errorType?: string;
|
|
465
468
|
errorMessage?: string;
|
|
466
469
|
/** Low-level transport code such as ETIMEDOUT or EADDRNOTAVAIL. */
|
|
@@ -522,6 +525,8 @@ export type AnthropicAttemptLogger = (status: number, errorType?: string, errorM
|
|
|
522
525
|
errorCode?: string;
|
|
523
526
|
rateLimitKind?: "transient" | "quota";
|
|
524
527
|
cooldownReason?: "transient" | "session" | "weekly" | "unified";
|
|
528
|
+
/** Override for nested retries whose attempt starts after this logger. */
|
|
529
|
+
attemptDurationMs?: number;
|
|
525
530
|
/** Override used when one account selection performs an OAuth retry fetch. */
|
|
526
531
|
attempt?: number;
|
|
527
532
|
}) => void;
|
|
@@ -558,9 +563,40 @@ export type LoadedClaudeAccountContext = {
|
|
|
558
563
|
};
|
|
559
564
|
export type AnthropicSuccessResult = {
|
|
560
565
|
retryNextAccount: true;
|
|
566
|
+
failure?: {
|
|
567
|
+
message: string;
|
|
568
|
+
rateLimit: boolean;
|
|
569
|
+
};
|
|
561
570
|
} | {
|
|
562
571
|
response: Response | unknown;
|
|
563
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
|
+
};
|
|
564
600
|
export type AnthropicAuthRetryResult = {
|
|
565
601
|
response?: Response | unknown;
|
|
566
602
|
continueLoop: boolean;
|
|
@@ -810,6 +846,8 @@ export type ProxyReadinessState = {
|
|
|
810
846
|
startTimeMs: number;
|
|
811
847
|
acceptingConnections: boolean;
|
|
812
848
|
ready: boolean;
|
|
849
|
+
/** True only while the updater is draining inference traffic. */
|
|
850
|
+
drainingForUpdate: boolean;
|
|
813
851
|
readyAtMs?: number;
|
|
814
852
|
};
|
|
815
853
|
/** Structured response returned by the proxy /health endpoint. */
|
|
@@ -817,6 +855,7 @@ export type ProxyHealthResponse = {
|
|
|
817
855
|
status: "ok" | "starting";
|
|
818
856
|
ready: boolean;
|
|
819
857
|
acceptingConnections: boolean;
|
|
858
|
+
drainingForUpdate: boolean;
|
|
820
859
|
strategy: string;
|
|
821
860
|
passthrough: boolean;
|
|
822
861
|
version: string;
|
|
@@ -1025,6 +1064,124 @@ export type QueuedProxyLifecycleEvent = {
|
|
|
1025
1064
|
date: string;
|
|
1026
1065
|
record: Record<string, unknown>;
|
|
1027
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
|
+
};
|
|
1028
1185
|
/** Request metadata retained by the HTTP adapter for terminal error logging. */
|
|
1029
1186
|
export type RuntimeRequestMetadata = {
|
|
1030
1187
|
requestId: string;
|
|
@@ -1034,6 +1191,8 @@ export type RuntimeRequestMetadata = {
|
|
|
1034
1191
|
model: string;
|
|
1035
1192
|
stream: boolean;
|
|
1036
1193
|
toolCount: number;
|
|
1194
|
+
/** Admission decision captured before an updater drain can race the route. */
|
|
1195
|
+
rejectForUpdate?: boolean;
|
|
1037
1196
|
terminalErrorType?: string;
|
|
1038
1197
|
terminalErrorCode?: string;
|
|
1039
1198
|
};
|
|
@@ -1201,6 +1360,14 @@ export type UpdateState = {
|
|
|
1201
1360
|
lastUpdateVersion: string | null;
|
|
1202
1361
|
/** Installed by the updater but not yet confirmed as the running version. */
|
|
1203
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;
|
|
1204
1371
|
/** Last updater failure, retained until a successful update or replacement. */
|
|
1205
1372
|
lastFailure: {
|
|
1206
1373
|
at: string;
|
|
@@ -1209,6 +1376,26 @@ export type UpdateState = {
|
|
|
1209
1376
|
message: string;
|
|
1210
1377
|
} | null;
|
|
1211
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
|
+
};
|
|
1212
1399
|
/** Result of validating a newly installed CLI through the stable trampoline. */
|
|
1213
1400
|
export type InstalledVersionValidation = {
|
|
1214
1401
|
version?: string;
|