@juspay/neurolink 10.8.10 → 10.8.12
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 +386 -386
- package/dist/cli/commands/proxy.d.ts +20 -1
- package/dist/cli/commands/proxy.js +73 -3
- package/dist/lib/proxy/updateState.d.ts +1 -1
- package/dist/lib/proxy/updateState.js +18 -1
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +7 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +88 -29
- package/dist/lib/types/cli.d.ts +2 -0
- package/dist/lib/types/proxy.d.ts +9 -0
- package/dist/proxy/updateState.d.ts +1 -1
- package/dist/proxy/updateState.js +18 -1
- package/dist/server/routes/claudeProxyRoutes.d.ts +7 -0
- package/dist/server/routes/claudeProxyRoutes.js +88 -29
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/proxy.d.ts +9 -0
- package/package.json +1 -1
|
@@ -11,8 +11,27 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { CommandModule } from "yargs";
|
|
13
13
|
import type { Hono } from "hono";
|
|
14
|
-
import type { AccountAllowlist, LoadedProxyConfig, ModelRouterInterface, ProxyGuardArgs, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxyStatusArgs, ProxyTelemetryArgs, ProxyReadinessState } from "../../lib/types/index.js";
|
|
14
|
+
import type { AccountAllowlist, LoadedProxyConfig, ModelRouterInterface, ProxyGuardArgs, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxySupervisorState, ProxyStatusArgs, ProxyTelemetryArgs, ProxyReadinessState } from "../../lib/types/index.js";
|
|
15
15
|
import { ProxyRuntimeConfigStore } from "../../lib/proxy/runtimeConfig.js";
|
|
16
|
+
/**
|
|
17
|
+
* Drop a supervisor `version` that is not a string.
|
|
18
|
+
*
|
|
19
|
+
* `StateFileManager.load()` is a bare `JSON.parse(content) as T` — it validates
|
|
20
|
+
* nothing. A state file written by a different build, or half-written during a
|
|
21
|
+
* crash, can carry any JSON type here, and every status renderer interpolates
|
|
22
|
+
* the field straight into `v${...}`. Coercing at the single load boundary keeps
|
|
23
|
+
* a non-string from reaching the output as "v[object Object]".
|
|
24
|
+
*/
|
|
25
|
+
export declare function normalizeSupervisorState(state: ProxySupervisorState | null): ProxySupervisorState | null;
|
|
26
|
+
/**
|
|
27
|
+
* Whether a rolling handoff can actually occur.
|
|
28
|
+
*
|
|
29
|
+
* A live supervisor PID alone is not enough: a supervisor from a build
|
|
30
|
+
* predating rolling state leaves `rolling` absent, and calling that a handoff
|
|
31
|
+
* makes both `/status` clients and the CLI wait for an activation that will
|
|
32
|
+
* never come. Gate on the capability, not on the process.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isRollingHandoffCapable(state: ProxySupervisorState | null, isRunning?: (pid: number) => boolean): boolean;
|
|
16
35
|
/**
|
|
17
36
|
* Best-effort check that a pid actually belongs to a neurolink proxy process,
|
|
18
37
|
* so a stale/recycled supervisor pid is never mistaken for a live supervisor
|
|
@@ -81,8 +81,42 @@ function clearProxyState() {
|
|
|
81
81
|
function saveProxySupervisorState(state) {
|
|
82
82
|
proxySupervisorStateManager.save(state);
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Drop a supervisor `version` that is not a string.
|
|
86
|
+
*
|
|
87
|
+
* `StateFileManager.load()` is a bare `JSON.parse(content) as T` — it validates
|
|
88
|
+
* nothing. A state file written by a different build, or half-written during a
|
|
89
|
+
* crash, can carry any JSON type here, and every status renderer interpolates
|
|
90
|
+
* the field straight into `v${...}`. Coercing at the single load boundary keeps
|
|
91
|
+
* a non-string from reaching the output as "v[object Object]".
|
|
92
|
+
*/
|
|
93
|
+
export function normalizeSupervisorState(state) {
|
|
94
|
+
if (!state) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return typeof state.version === "string" || state.version === undefined
|
|
98
|
+
? state
|
|
99
|
+
: { ...state, version: undefined };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether a rolling handoff can actually occur.
|
|
103
|
+
*
|
|
104
|
+
* A live supervisor PID alone is not enough: a supervisor from a build
|
|
105
|
+
* predating rolling state leaves `rolling` absent, and calling that a handoff
|
|
106
|
+
* makes both `/status` clients and the CLI wait for an activation that will
|
|
107
|
+
* never come. Gate on the capability, not on the process.
|
|
108
|
+
*/
|
|
109
|
+
export function isRollingHandoffCapable(state, isRunning = isProcessRunning) {
|
|
110
|
+
if (!state) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
// Structural, not just non-null: the same unvalidated `as T` load that lets
|
|
114
|
+
// `version` be an object lets `rolling` be a string.
|
|
115
|
+
const hasRollingState = typeof state.rolling === "object" && state.rolling !== null;
|
|
116
|
+
return isRunning(state.pid) && hasRollingState;
|
|
117
|
+
}
|
|
84
118
|
function loadProxySupervisorState() {
|
|
85
|
-
return proxySupervisorStateManager.load();
|
|
119
|
+
return normalizeSupervisorState(proxySupervisorStateManager.load());
|
|
86
120
|
}
|
|
87
121
|
function clearProxySupervisorState() {
|
|
88
122
|
proxySupervisorStateManager.clear();
|
|
@@ -1556,6 +1590,7 @@ export async function createProxyStartApp(params) {
|
|
|
1556
1590
|
: undefined;
|
|
1557
1591
|
const runtimeState = loadProxyState();
|
|
1558
1592
|
const supervisorState = loadProxySupervisorState();
|
|
1593
|
+
const rollingSupervisorRunning = isRollingHandoffCapable(supervisorState);
|
|
1559
1594
|
const updateState = loadUpdateState();
|
|
1560
1595
|
const cooldowns = await loadAccountCooldowns();
|
|
1561
1596
|
const storedAccountKeys = new Set();
|
|
@@ -1779,6 +1814,7 @@ export async function createProxyStartApp(params) {
|
|
|
1779
1814
|
autoUpdate: {
|
|
1780
1815
|
enabled: isProxyAutoUpdateEnabled(),
|
|
1781
1816
|
supervisorPid: supervisorState?.pid ?? null,
|
|
1817
|
+
supervisorVersion: supervisorState?.version ?? null,
|
|
1782
1818
|
rolling: supervisorState?.rolling ?? null,
|
|
1783
1819
|
updaterPid: activeUpdaterPid ?? null,
|
|
1784
1820
|
updaterRunning: activeUpdaterPid
|
|
@@ -1786,7 +1822,16 @@ export async function createProxyStartApp(params) {
|
|
|
1786
1822
|
: false,
|
|
1787
1823
|
liveVersion: PROXY_VERSION,
|
|
1788
1824
|
latestVersion: updateState?.lastCheckVersion || null,
|
|
1825
|
+
lastDetectedVersion: updateState?.lastCheckVersion || null,
|
|
1826
|
+
installedVersion: updateState?.installedVersion ??
|
|
1827
|
+
updateState?.lastUpdateVersion ??
|
|
1828
|
+
null,
|
|
1829
|
+
activatedVersion: PROXY_VERSION,
|
|
1830
|
+
pendingActivationVersion: updateState?.pendingRestartVersion ?? null,
|
|
1789
1831
|
pendingRestartVersion: updateState?.pendingRestartVersion ?? null,
|
|
1832
|
+
activationMode: rollingSupervisorRunning
|
|
1833
|
+
? "rolling-handoff"
|
|
1834
|
+
: "restart",
|
|
1790
1835
|
deferredUpdate: updateState?.deferredUpdate ?? null,
|
|
1791
1836
|
lastCheckAt: updateState?.lastCheckAt ?? null,
|
|
1792
1837
|
lastUpdateAt: updateState?.lastUpdateAt ?? null,
|
|
@@ -2426,6 +2471,7 @@ async function runLaunchdProxySupervisor(argv, spinner) {
|
|
|
2426
2471
|
host,
|
|
2427
2472
|
port,
|
|
2428
2473
|
startTime: supervisorStartedAt,
|
|
2474
|
+
version: PROXY_VERSION,
|
|
2429
2475
|
updaterPid: currentUpdaterPid,
|
|
2430
2476
|
rolling: snapshot,
|
|
2431
2477
|
});
|
|
@@ -2850,11 +2896,18 @@ export const proxyStatusCommand = {
|
|
|
2850
2896
|
autoUpdateEnabled: isProxyAutoUpdateEnabled(),
|
|
2851
2897
|
workerVersion: null,
|
|
2852
2898
|
supervisorPid: null,
|
|
2899
|
+
supervisorVersion: supervisorState?.version ?? null,
|
|
2853
2900
|
supervisorRunning: false,
|
|
2854
2901
|
rolling: null,
|
|
2855
2902
|
updaterPid: null,
|
|
2856
2903
|
updaterRunning: false,
|
|
2857
2904
|
latestVersion: updateState?.lastCheckVersion || null,
|
|
2905
|
+
lastDetectedVersion: updateState?.lastCheckVersion || null,
|
|
2906
|
+
installedVersion: updateState?.installedVersion ??
|
|
2907
|
+
updateState?.lastUpdateVersion ??
|
|
2908
|
+
null,
|
|
2909
|
+
activatedVersion: supervisorState?.rolling.active?.version ?? null,
|
|
2910
|
+
pendingActivationVersion: updateState?.pendingRestartVersion ?? null,
|
|
2858
2911
|
pendingRestartVersion: updateState?.pendingRestartVersion ?? null,
|
|
2859
2912
|
deferredUpdate: updateState?.deferredUpdate ?? null,
|
|
2860
2913
|
lastUpdateFailure: updateState?.lastFailure ?? null,
|
|
@@ -2904,6 +2957,7 @@ export const proxyStatusCommand = {
|
|
|
2904
2957
|
servingState?.lastConfigReloadError ?? null;
|
|
2905
2958
|
status.supervisorPid = supervisorPid ?? null;
|
|
2906
2959
|
status.supervisorRunning = supervisorRunning;
|
|
2960
|
+
status.supervisorVersion = supervisorState?.version ?? null;
|
|
2907
2961
|
status.rolling = supervisorState?.rolling ?? null;
|
|
2908
2962
|
status.updaterPid =
|
|
2909
2963
|
supervisorState?.updaterPid ?? servingState?.updaterPid ?? null;
|
|
@@ -2927,6 +2981,7 @@ export const proxyStatusCommand = {
|
|
|
2927
2981
|
typeof statusData.version === "string"
|
|
2928
2982
|
? statusData.version
|
|
2929
2983
|
: null;
|
|
2984
|
+
status.activatedVersion = status.workerVersion;
|
|
2930
2985
|
if (typeof liveConfig?.generation === "number") {
|
|
2931
2986
|
status.configGeneration = liveConfig.generation;
|
|
2932
2987
|
}
|
|
@@ -2959,6 +3014,9 @@ export const proxyStatusCommand = {
|
|
|
2959
3014
|
if (status.supervisorPid) {
|
|
2960
3015
|
logger.always(` ${chalk.bold("Supervisor:")} ${status.supervisorRunning ? chalk.cyan(status.supervisorPid) : chalk.red(`${status.supervisorPid} (not running)`)}`);
|
|
2961
3016
|
}
|
|
3017
|
+
if (status.supervisorVersion) {
|
|
3018
|
+
logger.always(` ${chalk.bold("Supervisor version:")} ${chalk.cyan(`v${status.supervisorVersion}`)}`);
|
|
3019
|
+
}
|
|
2962
3020
|
if (status.workerVersion) {
|
|
2963
3021
|
logger.always(` ${chalk.bold("Version:")} ${chalk.cyan(`v${status.workerVersion}`)}`);
|
|
2964
3022
|
}
|
|
@@ -2977,8 +3035,14 @@ export const proxyStatusCommand = {
|
|
|
2977
3035
|
logger.always(` ${chalk.bold("Started:")} ${chalk.cyan(status.startTime)}`);
|
|
2978
3036
|
logger.always(` ${chalk.bold("Uptime:")} ${chalk.cyan(formatUptime(status.uptime ?? 0))}`);
|
|
2979
3037
|
logger.always(` ${chalk.bold("Auto-update:")} ${status.autoUpdateEnabled ? chalk.green(status.updaterRunning ? `enabled (PID ${status.updaterPid})` : "enabled (worker unavailable)") : chalk.yellow("disabled")}`);
|
|
2980
|
-
if (status.
|
|
2981
|
-
|
|
3038
|
+
if (status.pendingActivationVersion) {
|
|
3039
|
+
// A live supervisor PID alone does NOT mean rolling handoff is
|
|
3040
|
+
// available: a supervisor from a build predating rolling state leaves
|
|
3041
|
+
// `rolling` absent, and calling that a handoff tells the operator to
|
|
3042
|
+
// wait for an activation that will never come. Gate on the capability,
|
|
3043
|
+
// not on the process.
|
|
3044
|
+
const rollingCapable = status.supervisorRunning && status.rolling !== null;
|
|
3045
|
+
logger.always(` ${chalk.bold(rollingCapable ? "Pending handoff:" : "Pending restart:")} ${chalk.yellow(`v${status.pendingActivationVersion} installed; ${rollingCapable ? "rolling activation pending" : "restart pending"}`)}`);
|
|
2982
3046
|
}
|
|
2983
3047
|
if (status.rolling?.candidate) {
|
|
2984
3048
|
logger.always(` ${chalk.bold("Handoff:")} ${chalk.yellow(`preparing v${status.rolling.candidate.expectedVersion} (PID ${status.rolling.candidate.pid})`)}`);
|
|
@@ -2995,6 +3059,12 @@ export const proxyStatusCommand = {
|
|
|
2995
3059
|
if (status.latestVersion) {
|
|
2996
3060
|
logger.always(` ${chalk.bold("Latest:")} ${chalk.cyan(`v${status.latestVersion}`)}`);
|
|
2997
3061
|
}
|
|
3062
|
+
if (status.installedVersion) {
|
|
3063
|
+
logger.always(` ${chalk.bold("Installed:")} ${chalk.cyan(`v${status.installedVersion}`)}`);
|
|
3064
|
+
}
|
|
3065
|
+
if (status.activatedVersion) {
|
|
3066
|
+
logger.always(` ${chalk.bold("Activated:")} ${chalk.cyan(`v${status.activatedVersion}`)}`);
|
|
3067
|
+
}
|
|
2998
3068
|
if (status.lastUpdateFailure) {
|
|
2999
3069
|
logger.always(` ${chalk.bold("Update error:")} ${chalk.red(`${status.lastUpdateFailure.stage}: ${status.lastUpdateFailure.message}`)}`);
|
|
3000
3070
|
}
|
|
@@ -48,7 +48,7 @@ export declare function suppressVersion(version: string, reason: string, stateFi
|
|
|
48
48
|
* @param stateFilePath - Override path for testing
|
|
49
49
|
*/
|
|
50
50
|
export declare function recordSuccessfulUpdate(version: string, stateFilePath?: string): void;
|
|
51
|
-
/** Record that package
|
|
51
|
+
/** Record that the package was validated but live activation is still pending. */
|
|
52
52
|
export declare function recordUpdateInstalled(version: string, stateFilePath?: string): void;
|
|
53
53
|
/** Abandon a matching installed version so the next cycle may reinstall it. */
|
|
54
54
|
export declare function abandonPendingUpdate(version: string, stateFilePath?: string): boolean;
|
|
@@ -87,6 +87,7 @@ export function getDefaultUpdateState() {
|
|
|
87
87
|
lastCheckAt: new Date(0).toISOString(),
|
|
88
88
|
lastCheckVersion: "",
|
|
89
89
|
suppressedVersions: {},
|
|
90
|
+
installedVersion: null,
|
|
90
91
|
lastUpdateAt: null,
|
|
91
92
|
lastUpdateVersion: null,
|
|
92
93
|
pendingRestartVersion: null,
|
|
@@ -124,6 +125,20 @@ export function loadUpdateState(stateFilePath) {
|
|
|
124
125
|
...getDefaultUpdateState(),
|
|
125
126
|
...candidate,
|
|
126
127
|
suppressedVersions: candidate.suppressedVersions ?? {},
|
|
128
|
+
// Backfill order matters for state files written before `installedVersion`
|
|
129
|
+
// existed. Back then `recordUpdateInstalled()` set ONLY
|
|
130
|
+
// `pendingRestartVersion`, leaving `lastUpdateVersion` on the previously
|
|
131
|
+
// activated build — so a validated-but-not-yet-running update lives in
|
|
132
|
+
// `pendingRestartVersion` and is the newer of the two. Reading
|
|
133
|
+
// `lastUpdateVersion` first would report the superseded version as
|
|
134
|
+
// installed and re-offer an update that is already on disk.
|
|
135
|
+
installedVersion: typeof candidate.installedVersion === "string"
|
|
136
|
+
? candidate.installedVersion
|
|
137
|
+
: typeof candidate.pendingRestartVersion === "string"
|
|
138
|
+
? candidate.pendingRestartVersion
|
|
139
|
+
: typeof candidate.lastUpdateVersion === "string"
|
|
140
|
+
? candidate.lastUpdateVersion
|
|
141
|
+
: null,
|
|
127
142
|
pendingRestartVersion: typeof candidate.pendingRestartVersion === "string"
|
|
128
143
|
? candidate.pendingRestartVersion
|
|
129
144
|
: null,
|
|
@@ -208,15 +223,17 @@ export function recordSuccessfulUpdate(version, stateFilePath) {
|
|
|
208
223
|
const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
|
|
209
224
|
state.lastUpdateAt = new Date().toISOString();
|
|
210
225
|
state.lastUpdateVersion = version;
|
|
226
|
+
state.installedVersion = version;
|
|
211
227
|
state.pendingRestartVersion = null;
|
|
212
228
|
state.deferredUpdate = null;
|
|
213
229
|
state.lastFailure = null;
|
|
214
230
|
delete state.suppressedVersions[version];
|
|
215
231
|
saveUpdateState(state, stateFilePath);
|
|
216
232
|
}
|
|
217
|
-
/** Record that package
|
|
233
|
+
/** Record that the package was validated but live activation is still pending. */
|
|
218
234
|
export function recordUpdateInstalled(version, stateFilePath) {
|
|
219
235
|
const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
|
|
236
|
+
state.installedVersion = version;
|
|
220
237
|
state.pendingRestartVersion = version;
|
|
221
238
|
state.lastFailure = null;
|
|
222
239
|
saveUpdateState(state, stateFilePath);
|
|
@@ -297,6 +297,12 @@ export declare function createClaudeProxyRoutes(modelRouter?: ModelRouterInterfa
|
|
|
297
297
|
declare function reconcileEligibleAccountRuntimeState(account: ProxyPassthroughAccount): void;
|
|
298
298
|
export declare function getTransientSameAccountRetryDelayMs(retryNumber: number): number;
|
|
299
299
|
declare function describeTransportError(error: unknown): string;
|
|
300
|
+
/**
|
|
301
|
+
* Determine whether a POST can be retried without risking duplicate provider
|
|
302
|
+
* work. Only failures that prove connection establishment did not complete are
|
|
303
|
+
* safe; a reset, socket error, or response timeout can happen after dispatch.
|
|
304
|
+
*/
|
|
305
|
+
declare function isRetryableNetworkError(error: unknown): boolean;
|
|
300
306
|
/**
|
|
301
307
|
* Parse a Claude error payload when available.
|
|
302
308
|
*/
|
|
@@ -338,6 +344,7 @@ export declare const __testHooks: {
|
|
|
338
344
|
maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
|
|
339
345
|
planCooldownFor429: typeof planCooldownFor429;
|
|
340
346
|
reconcileCooldownFromQuota: typeof reconcileCooldownFromQuota;
|
|
347
|
+
isRetryableNetworkError: typeof isRetryableNetworkError;
|
|
341
348
|
isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
|
|
342
349
|
getStreamFailureDetails: typeof getStreamFailureDetails;
|
|
343
350
|
trackUpstreamReadableStream: typeof trackUpstreamReadableStream;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { access, readFile } from "node:fs/promises";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
+
import { Agent } from "undici";
|
|
15
16
|
import { buildStableClaudeCodeBillingHeader, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_OAUTH_BETAS, getOrCreateClaudeCodeIdentity, parseClaudeCodeUserId, } from "../../auth/anthropicOAuth.js";
|
|
16
17
|
import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from "../../proxy/accountCooldown.js";
|
|
17
18
|
import { anthropicAccountKeysEqual, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
|
|
@@ -91,6 +92,21 @@ const AUTH_REFRESH_MAX_COOLDOWN_MS = 5 * 60 * 1000;
|
|
|
91
92
|
* to cover the full lifecycle of streaming responses, including extended
|
|
92
93
|
* thinking from Opus models (which can exceed 5 minutes for large contexts). */
|
|
93
94
|
const UPSTREAM_FETCH_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
|
|
95
|
+
let anthropicUpstreamDispatcher;
|
|
96
|
+
function fetchAnthropicUpstream(url, init) {
|
|
97
|
+
// Node's global fetch applies Undici's 300s default headers timeout before
|
|
98
|
+
// the route's 15-minute abort signal. Keep both transport deadlines aligned
|
|
99
|
+
// with the proxy contract and instantiate lazily so importing routes has no
|
|
100
|
+
// open transport handles.
|
|
101
|
+
anthropicUpstreamDispatcher ??= new Agent({
|
|
102
|
+
headersTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
|
|
103
|
+
bodyTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
|
|
104
|
+
});
|
|
105
|
+
return fetch(url, {
|
|
106
|
+
...init,
|
|
107
|
+
dispatcher: anthropicUpstreamDispatcher,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
94
110
|
const accountRuntimeState = new Map();
|
|
95
111
|
/** Shared across requests so a concurrent burst gets at most two retries for
|
|
96
112
|
* the account/window, rather than every request starting its own retry chain. */
|
|
@@ -1406,7 +1422,7 @@ async function handleClaudePassthroughRequest(args) {
|
|
|
1406
1422
|
recordAttempt("passthrough", "passthrough");
|
|
1407
1423
|
let response;
|
|
1408
1424
|
try {
|
|
1409
|
-
response = await
|
|
1425
|
+
response = await fetchAnthropicUpstream("https://api.anthropic.com/v1/messages?beta=true", {
|
|
1410
1426
|
method: "POST",
|
|
1411
1427
|
headers: upstreamHeaders,
|
|
1412
1428
|
body: bodyStr,
|
|
@@ -2510,10 +2526,11 @@ async function handleAnthropicStreamingSuccessResponse(args) {
|
|
|
2510
2526
|
if (preflight.kind === "transport_error") {
|
|
2511
2527
|
const message = describeTransportError(preflight.error);
|
|
2512
2528
|
const partialBody = Buffer.concat(preflight.chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
2513
|
-
|
|
2529
|
+
// The POST has already returned a response. The upstream may have started
|
|
2530
|
+
// processing it, so replaying it on another account could duplicate work.
|
|
2531
|
+
logger.always(`[proxy] stream failed before first chunk account=${account.label}: ${message}; returning terminal error to avoid replaying an ambiguous request`);
|
|
2514
2532
|
recordAttemptError(account.label, account.type, 502);
|
|
2515
|
-
logAttempt(502, "stream_error", message, { retryable:
|
|
2516
|
-
tracer?.recordRetry(account.label, "stream_before_first_chunk");
|
|
2533
|
+
logAttempt(502, "stream_error", message, { retryable: false });
|
|
2517
2534
|
upstreamSpan?.end();
|
|
2518
2535
|
logProxyBody({
|
|
2519
2536
|
phase: "upstream_response",
|
|
@@ -2529,8 +2546,16 @@ async function handleAnthropicStreamingSuccessResponse(args) {
|
|
|
2529
2546
|
metadata: { logicalStatus: 502, transportError: message },
|
|
2530
2547
|
});
|
|
2531
2548
|
return {
|
|
2532
|
-
|
|
2533
|
-
|
|
2549
|
+
response: finalizeAnthropicTerminalTransportError({
|
|
2550
|
+
account,
|
|
2551
|
+
tracer,
|
|
2552
|
+
requestStartTime,
|
|
2553
|
+
attemptNumber,
|
|
2554
|
+
logProxyBody,
|
|
2555
|
+
logFinalRequest,
|
|
2556
|
+
errorType: "stream_error",
|
|
2557
|
+
message,
|
|
2558
|
+
}),
|
|
2534
2559
|
};
|
|
2535
2560
|
}
|
|
2536
2561
|
if (preflight.kind === "empty") {
|
|
@@ -3176,7 +3201,7 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
3176
3201
|
metadata: { upstreamMethod: "POST", upstreamUrl: url },
|
|
3177
3202
|
});
|
|
3178
3203
|
try {
|
|
3179
|
-
const retryResp = await
|
|
3204
|
+
const retryResp = await fetchAnthropicUpstream(url, {
|
|
3180
3205
|
method: "POST",
|
|
3181
3206
|
headers,
|
|
3182
3207
|
body: retryBodyStr,
|
|
@@ -3387,11 +3412,37 @@ async function handleAnthropicAuthRetry(args) {
|
|
|
3387
3412
|
: String(retryFetchErr);
|
|
3388
3413
|
authRetryError = `network error on retry ${authRetry + 1}: ${message}`;
|
|
3389
3414
|
currentLastError = authRetryError;
|
|
3415
|
+
const retryable = isRetryableNetworkError(retryFetchErr);
|
|
3390
3416
|
retryLogAttempt(502, "network_error", message, {
|
|
3391
|
-
retryable
|
|
3417
|
+
retryable,
|
|
3392
3418
|
errorCode: getErrorCode(retryFetchErr) ?? "unknown",
|
|
3393
3419
|
});
|
|
3394
3420
|
logger.debug(`[proxy] ${authRetryError}`);
|
|
3421
|
+
if (!retryable) {
|
|
3422
|
+
// Once a POST has left this process, a reset/timeout or unknown fetch
|
|
3423
|
+
// failure is ambiguous: retrying it on another account can duplicate
|
|
3424
|
+
// the request. Only connection-establishment failures are replay-safe.
|
|
3425
|
+
currentUpstreamSpan?.end();
|
|
3426
|
+
return {
|
|
3427
|
+
response: finalizeAnthropicTerminalTransportError({
|
|
3428
|
+
account,
|
|
3429
|
+
tracer,
|
|
3430
|
+
requestStartTime,
|
|
3431
|
+
attemptNumber: retryAttemptNumber,
|
|
3432
|
+
logProxyBody,
|
|
3433
|
+
logFinalRequest,
|
|
3434
|
+
errorType: "network_error",
|
|
3435
|
+
message,
|
|
3436
|
+
}),
|
|
3437
|
+
continueLoop: false,
|
|
3438
|
+
lastError: currentLastError,
|
|
3439
|
+
authFailureMessage: currentAuthFailureMessage,
|
|
3440
|
+
sawRateLimit: currentSawRateLimit,
|
|
3441
|
+
sawTransientFailure: currentSawTransientFailure,
|
|
3442
|
+
sawNetworkError: currentSawNetworkError,
|
|
3443
|
+
upstreamSpan: undefined,
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3395
3446
|
break;
|
|
3396
3447
|
}
|
|
3397
3448
|
}
|
|
@@ -3470,6 +3521,27 @@ function finalizeAnthropicTerminalFetchError(args) {
|
|
|
3470
3521
|
errorType: terminalError.errorType,
|
|
3471
3522
|
});
|
|
3472
3523
|
}
|
|
3524
|
+
function finalizeAnthropicTerminalTransportError(args) {
|
|
3525
|
+
const { account, tracer, requestStartTime, attemptNumber, logProxyBody, logFinalRequest, errorType, message, } = args;
|
|
3526
|
+
tracer?.setError(errorType, message);
|
|
3527
|
+
tracer?.end(502, Date.now() - requestStartTime);
|
|
3528
|
+
logFinalRequest(502, account.label, account.type, errorType, message);
|
|
3529
|
+
const clientError = buildClaudeError(502, message);
|
|
3530
|
+
const clientErrorBody = JSON.stringify(clientError);
|
|
3531
|
+
logProxyBody({
|
|
3532
|
+
phase: "client_response",
|
|
3533
|
+
headers: { "content-type": "application/json" },
|
|
3534
|
+
body: clientErrorBody,
|
|
3535
|
+
bodySize: Buffer.byteLength(clientErrorBody, "utf8"),
|
|
3536
|
+
contentType: "application/json",
|
|
3537
|
+
account: account.label,
|
|
3538
|
+
accountType: account.type,
|
|
3539
|
+
attempt: attemptNumber,
|
|
3540
|
+
responseStatus: 502,
|
|
3541
|
+
durationMs: Date.now() - requestStartTime,
|
|
3542
|
+
});
|
|
3543
|
+
return clientError;
|
|
3544
|
+
}
|
|
3473
3545
|
async function handleAnthropicNonOkResponse(args) {
|
|
3474
3546
|
const { response, account, accountState, enabledAccounts, orderedAccounts, tracer, requestStartTime, fetchStartMs, attemptNumber, logAttempt, logProxyBody, logFinalRequest, lastError, authFailureMessage, sawTransientFailure, invalidRequestFailure, } = args;
|
|
3475
3547
|
let currentLastError = lastError;
|
|
@@ -4043,7 +4115,7 @@ async function fetchAnthropicAccountResponse(args) {
|
|
|
4043
4115
|
const currentUpstreamSpan = upstreamSpan;
|
|
4044
4116
|
let response;
|
|
4045
4117
|
try {
|
|
4046
|
-
response = await
|
|
4118
|
+
response = await fetchAnthropicUpstream(url, {
|
|
4047
4119
|
method: "POST",
|
|
4048
4120
|
headers,
|
|
4049
4121
|
body: finalBodyStr,
|
|
@@ -4998,37 +5070,23 @@ function describeTransportError(error) {
|
|
|
4998
5070
|
return detail ? `${message} (${detail})` : message;
|
|
4999
5071
|
}
|
|
5000
5072
|
/**
|
|
5001
|
-
* Determine whether a
|
|
5073
|
+
* Determine whether a POST can be retried without risking duplicate provider
|
|
5074
|
+
* work. Only failures that prove connection establishment did not complete are
|
|
5075
|
+
* safe; a reset, socket error, or response timeout can happen after dispatch.
|
|
5002
5076
|
*/
|
|
5003
5077
|
function isRetryableNetworkError(error) {
|
|
5004
5078
|
const code = getErrorCode(error);
|
|
5005
|
-
|
|
5079
|
+
return (code !== undefined &&
|
|
5006
5080
|
[
|
|
5007
5081
|
"ECONNREFUSED",
|
|
5008
|
-
"
|
|
5082
|
+
"EADDRNOTAVAIL",
|
|
5009
5083
|
// The Anthropic host is fixed, so ENOTFOUND can be a transient resolver
|
|
5010
5084
|
// outage. Keep it inside the existing bounded same-account retry budget.
|
|
5011
5085
|
"ENOTFOUND",
|
|
5012
|
-
"ETIMEDOUT",
|
|
5013
5086
|
"EHOSTUNREACH",
|
|
5014
5087
|
"UND_ERR_CONNECT_TIMEOUT",
|
|
5015
5088
|
"UND_ERR_CONNECT",
|
|
5016
|
-
|
|
5017
|
-
"UND_ERR_HEADERS_TIMEOUT",
|
|
5018
|
-
].includes(code)) {
|
|
5019
|
-
return true;
|
|
5020
|
-
}
|
|
5021
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
5022
|
-
const normalized = message.toLowerCase();
|
|
5023
|
-
return (normalized.includes("econnrefused") ||
|
|
5024
|
-
normalized.includes("econnreset") ||
|
|
5025
|
-
normalized.includes("enotfound") ||
|
|
5026
|
-
normalized.includes("etimedout") ||
|
|
5027
|
-
normalized.includes("timed out") ||
|
|
5028
|
-
normalized.includes("connection error") ||
|
|
5029
|
-
normalized.includes("connect error") ||
|
|
5030
|
-
normalized.includes("fetch failed") ||
|
|
5031
|
-
normalized.includes("socket hang up"));
|
|
5089
|
+
].includes(code));
|
|
5032
5090
|
}
|
|
5033
5091
|
const TRANSIENT_HTTP_STATUSES = new Set([
|
|
5034
5092
|
408, 500, 502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 529,
|
|
@@ -5158,6 +5216,7 @@ export const __testHooks = {
|
|
|
5158
5216
|
maybeResetPrimaryToHome,
|
|
5159
5217
|
planCooldownFor429,
|
|
5160
5218
|
reconcileCooldownFromQuota,
|
|
5219
|
+
isRetryableNetworkError,
|
|
5161
5220
|
isPermanentRefreshFailure,
|
|
5162
5221
|
getStreamFailureDetails,
|
|
5163
5222
|
trackUpstreamReadableStream,
|
package/dist/lib/types/cli.d.ts
CHANGED
|
@@ -1724,6 +1724,15 @@ export type UpdateState = {
|
|
|
1724
1724
|
lastCheckAt: string;
|
|
1725
1725
|
lastCheckVersion: string;
|
|
1726
1726
|
suppressedVersions: Record<string, SuppressedVersion>;
|
|
1727
|
+
/**
|
|
1728
|
+
* Last package version whose stable trampoline was successfully validated.
|
|
1729
|
+
*
|
|
1730
|
+
* Optional because `UpdateState` is part of the published type surface and a
|
|
1731
|
+
* required addition would break every downstream object literal — and because
|
|
1732
|
+
* state files written before this field existed legitimately omit it.
|
|
1733
|
+
* `loadUpdateState()` always materializes it, so runtime readers see a value.
|
|
1734
|
+
*/
|
|
1735
|
+
installedVersion?: string | null;
|
|
1727
1736
|
lastUpdateAt: string | null;
|
|
1728
1737
|
lastUpdateVersion: string | null;
|
|
1729
1738
|
/** Installed by the updater but not yet confirmed as the running version. */
|
|
@@ -48,7 +48,7 @@ export declare function suppressVersion(version: string, reason: string, stateFi
|
|
|
48
48
|
* @param stateFilePath - Override path for testing
|
|
49
49
|
*/
|
|
50
50
|
export declare function recordSuccessfulUpdate(version: string, stateFilePath?: string): void;
|
|
51
|
-
/** Record that package
|
|
51
|
+
/** Record that the package was validated but live activation is still pending. */
|
|
52
52
|
export declare function recordUpdateInstalled(version: string, stateFilePath?: string): void;
|
|
53
53
|
/** Abandon a matching installed version so the next cycle may reinstall it. */
|
|
54
54
|
export declare function abandonPendingUpdate(version: string, stateFilePath?: string): boolean;
|
|
@@ -87,6 +87,7 @@ export function getDefaultUpdateState() {
|
|
|
87
87
|
lastCheckAt: new Date(0).toISOString(),
|
|
88
88
|
lastCheckVersion: "",
|
|
89
89
|
suppressedVersions: {},
|
|
90
|
+
installedVersion: null,
|
|
90
91
|
lastUpdateAt: null,
|
|
91
92
|
lastUpdateVersion: null,
|
|
92
93
|
pendingRestartVersion: null,
|
|
@@ -124,6 +125,20 @@ export function loadUpdateState(stateFilePath) {
|
|
|
124
125
|
...getDefaultUpdateState(),
|
|
125
126
|
...candidate,
|
|
126
127
|
suppressedVersions: candidate.suppressedVersions ?? {},
|
|
128
|
+
// Backfill order matters for state files written before `installedVersion`
|
|
129
|
+
// existed. Back then `recordUpdateInstalled()` set ONLY
|
|
130
|
+
// `pendingRestartVersion`, leaving `lastUpdateVersion` on the previously
|
|
131
|
+
// activated build — so a validated-but-not-yet-running update lives in
|
|
132
|
+
// `pendingRestartVersion` and is the newer of the two. Reading
|
|
133
|
+
// `lastUpdateVersion` first would report the superseded version as
|
|
134
|
+
// installed and re-offer an update that is already on disk.
|
|
135
|
+
installedVersion: typeof candidate.installedVersion === "string"
|
|
136
|
+
? candidate.installedVersion
|
|
137
|
+
: typeof candidate.pendingRestartVersion === "string"
|
|
138
|
+
? candidate.pendingRestartVersion
|
|
139
|
+
: typeof candidate.lastUpdateVersion === "string"
|
|
140
|
+
? candidate.lastUpdateVersion
|
|
141
|
+
: null,
|
|
127
142
|
pendingRestartVersion: typeof candidate.pendingRestartVersion === "string"
|
|
128
143
|
? candidate.pendingRestartVersion
|
|
129
144
|
: null,
|
|
@@ -208,15 +223,17 @@ export function recordSuccessfulUpdate(version, stateFilePath) {
|
|
|
208
223
|
const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
|
|
209
224
|
state.lastUpdateAt = new Date().toISOString();
|
|
210
225
|
state.lastUpdateVersion = version;
|
|
226
|
+
state.installedVersion = version;
|
|
211
227
|
state.pendingRestartVersion = null;
|
|
212
228
|
state.deferredUpdate = null;
|
|
213
229
|
state.lastFailure = null;
|
|
214
230
|
delete state.suppressedVersions[version];
|
|
215
231
|
saveUpdateState(state, stateFilePath);
|
|
216
232
|
}
|
|
217
|
-
/** Record that package
|
|
233
|
+
/** Record that the package was validated but live activation is still pending. */
|
|
218
234
|
export function recordUpdateInstalled(version, stateFilePath) {
|
|
219
235
|
const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
|
|
236
|
+
state.installedVersion = version;
|
|
220
237
|
state.pendingRestartVersion = version;
|
|
221
238
|
state.lastFailure = null;
|
|
222
239
|
saveUpdateState(state, stateFilePath);
|
|
@@ -297,6 +297,12 @@ export declare function createClaudeProxyRoutes(modelRouter?: ModelRouterInterfa
|
|
|
297
297
|
declare function reconcileEligibleAccountRuntimeState(account: ProxyPassthroughAccount): void;
|
|
298
298
|
export declare function getTransientSameAccountRetryDelayMs(retryNumber: number): number;
|
|
299
299
|
declare function describeTransportError(error: unknown): string;
|
|
300
|
+
/**
|
|
301
|
+
* Determine whether a POST can be retried without risking duplicate provider
|
|
302
|
+
* work. Only failures that prove connection establishment did not complete are
|
|
303
|
+
* safe; a reset, socket error, or response timeout can happen after dispatch.
|
|
304
|
+
*/
|
|
305
|
+
declare function isRetryableNetworkError(error: unknown): boolean;
|
|
300
306
|
/**
|
|
301
307
|
* Parse a Claude error payload when available.
|
|
302
308
|
*/
|
|
@@ -338,6 +344,7 @@ export declare const __testHooks: {
|
|
|
338
344
|
maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
|
|
339
345
|
planCooldownFor429: typeof planCooldownFor429;
|
|
340
346
|
reconcileCooldownFromQuota: typeof reconcileCooldownFromQuota;
|
|
347
|
+
isRetryableNetworkError: typeof isRetryableNetworkError;
|
|
341
348
|
isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
|
|
342
349
|
getStreamFailureDetails: typeof getStreamFailureDetails;
|
|
343
350
|
trackUpstreamReadableStream: typeof trackUpstreamReadableStream;
|