@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
|
@@ -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/types/cli.d.ts
CHANGED
package/dist/types/proxy.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. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "10.8.
|
|
3
|
+
"version": "10.8.12",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|