@juspay/neurolink 13.8.5 → 13.8.7

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.
@@ -56,7 +56,7 @@ import { verifySharePayload } from "../../proxy/shareSigning.js";
56
56
  import { availableCoins, readSharePoolWindowUsage, readShareWindowUsage, recordShareWindowDelta, settleShareUsage, usageToCoins, } from "../../proxy/shareLedger.js";
57
57
  import { accountsInGrantScope, filterAccountsForGrant, isModelAllowed, isWithinSchedule, shareRefusalStatus, summarizeAccountExclusions, } from "../../proxy/sharePolicy.js";
58
58
  import { createStreamTerminalOutcomeTracker, mergeStreamTerminalOutcome, preflightAnthropicStream, } from "../../proxy/streamOutcome.js";
59
- import { isPermanentRefreshFailure, needsRefresh, persistTokens, refreshToken, refreshTokenFromLatest, } from "../../proxy/tokenRefresh.js";
59
+ import { isPermanentRefreshFailure, needsInlineRefresh, persistTokens, refreshToken, refreshTokenFromLatest, } from "../../proxy/tokenRefresh.js";
60
60
  import { buildProxyTranslationPlan, parseRetryAfterMs, } from "../../proxy/routingPolicy.js";
61
61
  import { normalizeMaxInflightPerAccount } from "../../proxy/modelRouter.js";
62
62
  import { writeJsonSnapshotAtomically } from "../../proxy/snapshotPersistence.js";
@@ -166,10 +166,21 @@ const AUTH_REFRESH_MAX_COOLDOWN_MS = 5 * 60 * 1000;
166
166
  * to cover the full lifecycle of streaming responses, including extended
167
167
  * thinking from Opus models (which can exceed 5 minutes for large contexts). */
168
168
  const UPSTREAM_FETCH_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
169
+ /** Per-address connect attempt budget. Node's 250ms default turns one SYN that
170
+ * needs a retransmit into ETIMEDOUT on a host whose AAAA record has no route,
171
+ * because the IPv6 fallback then fails instantly. */
172
+ const UPSTREAM_CONNECT_ATTEMPT_TIMEOUT_MS = 2_000;
173
+ const UPSTREAM_CONNECT_TIMEOUT_MS = 10_000;
174
+ /** Idle upstream sockets are kept this long. undici's 4s default reopened TCP
175
+ * and TLS after nearly every pause between turns; Anthropic was measured
176
+ * keeping idle sockets open past 300s, so 30s reuses them with margin. */
177
+ const UPSTREAM_KEEP_ALIVE_TIMEOUT_MS = 30_000;
178
+ const UPSTREAM_KEEP_ALIVE_MAX_TIMEOUT_MS = 10 * 60 * 1000;
169
179
  // Context checks and spending reservations belong immediately before dispatch.
170
180
  import { prepareProxyRequestContext, ProxyContextPreflightError, } from "../../proxy/proxyContextPreflight.js";
171
181
  import { reserveProxyTokenBudget, getProxyTokenBudgetError, getProxyTokenBudgetSessionKey, } from "../../proxy/proxyTokenBudget.js";
172
182
  import { observeAnthropicBudgetResponse } from "../../proxy/anthropicBudgetResponse.js";
183
+ import { fetchProxyAware } from "../../proxy/proxyFetch.js";
173
184
  import { readCacheCreation1hTokens, oneHourCacheWriteFields, } from "../../proxy/proxyTokenUsage.js";
174
185
  function prepareAnthropicWire(ctx, bodyStr) {
175
186
  const body = JSON.parse(bodyStr);
@@ -209,13 +220,30 @@ async function getAnthropicUpstreamDispatcher() {
209
220
  }
210
221
  anthropicUpstreamDispatcherPromise ??= (async () => {
211
222
  try {
212
- const { Agent: UndiciAgent } = await import("undici");
223
+ const { Agent: UndiciAgent, buildConnector } = await import("undici");
224
+ const connectUpstream = buildConnector({
225
+ timeout: UPSTREAM_CONNECT_TIMEOUT_MS,
226
+ autoSelectFamily: true,
227
+ autoSelectFamilyAttemptTimeout: UPSTREAM_CONNECT_ATTEMPT_TIMEOUT_MS,
228
+ });
213
229
  // Node's global fetch applies Undici's 300s default headers timeout
214
230
  // before the route's 15-minute abort signal. Keep both transport
215
231
  // deadlines aligned with the proxy contract.
216
232
  const agent = new UndiciAgent({
217
233
  headersTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
218
234
  bodyTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
235
+ keepAliveTimeout: UPSTREAM_KEEP_ALIVE_TIMEOUT_MS,
236
+ keepAliveMaxTimeout: UPSTREAM_KEEP_ALIVE_MAX_TIMEOUT_MS,
237
+ // A completed connect is the earliest proof the provider transport is
238
+ // back, so it releases requests parked behind a recovery probe.
239
+ connect: (options, callback) => {
240
+ connectUpstream(options, (...result) => {
241
+ if (result[0] === null) {
242
+ providerTransportCoordinator.reportConnectionEstablished();
243
+ }
244
+ callback(...result);
245
+ });
246
+ },
219
247
  });
220
248
  anthropicUpstreamDispatcher = agent;
221
249
  return agent;
@@ -265,10 +293,12 @@ async function fetchAnthropicUpstream(url, init, ctx, accountKey, dispatch) {
265
293
  attempt: dispatch.attemptNumber,
266
294
  metadata: { upstreamMethod: "POST", upstreamUrl: url },
267
295
  });
268
- const response = await fetch(url, {
296
+ const response = await fetchProxyAware(url, {
269
297
  ...init,
270
298
  body: dispatchedBody,
271
- dispatcher: upstreamDispatcher,
299
+ }, upstreamDispatcher, {
300
+ headersTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
301
+ bodyTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
272
302
  });
273
303
  return observeAnthropicBudgetResponse(response, lease, () => {
274
304
  ctx.metadata.tokenBudget = lease.snapshot;
@@ -983,6 +1013,12 @@ async function seedRuntimeQuotasFromDisk(accounts) {
983
1013
  const MIN_USAGE_REFETCH_INTERVAL_MS = 15_000;
984
1014
  const lastUsageFetchAt = new Map();
985
1015
  let limitsRefreshInFlight = null;
1016
+ /** Status lines and dashboards poll `/limits` more often than requests arrive.
1017
+ * A sweep finished this recently is still current: usage is refetched at most
1018
+ * every MIN_USAGE_REFETCH_INTERVAL_MS anyway, and each sweep takes the token
1019
+ * store lock that `/v1/messages` waits on. */
1020
+ const LIMITS_RESULT_REUSE_MS = 2_000;
1021
+ let lastLimitsRefresh;
986
1022
  const USAGE_REFRESH_CONCURRENCY = 4;
987
1023
  async function applyAccountUsageResult(account, fetchResult, observedAt, prior) {
988
1024
  if (fetchResult.ok === false) {
@@ -2220,10 +2256,6 @@ function snapshotsMatch(existing, next) {
2220
2256
  async function persistClaudeSnapshot(accountLabel, snapshot) {
2221
2257
  const snapshotPath = getSnapshotPath(accountLabel);
2222
2258
  await writeJsonSnapshotAtomically(snapshotPath, snapshot, 0o600);
2223
- snapshotCache.set(getSnapshotSafeLabel(accountLabel), {
2224
- snapshot,
2225
- loadedAt: Date.now(),
2226
- });
2227
2259
  }
2228
2260
  async function maybeRefreshClaudeSnapshot(accountLabel, accountKey, headers, bodyStr) {
2229
2261
  const existing = await loadClaudeSnapshot(accountLabel);
@@ -2254,19 +2286,22 @@ async function maybeRefreshClaudeSnapshot(accountLabel, accountKey, headers, bod
2254
2286
  if (snapshotsMatch(existing, next)) {
2255
2287
  return existing;
2256
2288
  }
2257
- try {
2258
- await persistClaudeSnapshot(accountLabel, next);
2259
- }
2260
- catch (error) {
2289
+ // The request reads the snapshot from this cache, so it can go upstream now;
2290
+ // the file only matters to a later process start. Writes to one account's
2291
+ // file are already serialized, so the newest snapshot still lands last, and
2292
+ // proxy shutdown awaits flushSnapshotWrites() so a graceful exit keeps it.
2293
+ // A hard kill can still lose it, as a failed write always could; the next
2294
+ // Claude Code request recaptures it.
2295
+ snapshotCache.set(getSnapshotSafeLabel(accountLabel), {
2296
+ snapshot: next,
2297
+ loadedAt: Date.now(),
2298
+ });
2299
+ void persistClaudeSnapshot(accountLabel, next).catch((error) => {
2261
2300
  logger.warn("[proxy] failed to persist Claude snapshot", {
2262
2301
  accountLabel,
2263
2302
  error: error instanceof Error ? error.message : String(error),
2264
2303
  });
2265
- snapshotCache.set(getSnapshotSafeLabel(accountLabel), {
2266
- snapshot: next,
2267
- loadedAt: Date.now(),
2268
- });
2269
- }
2304
+ });
2270
2305
  return next;
2271
2306
  }
2272
2307
  /**
@@ -4331,9 +4366,9 @@ function getCodexFallbackInvalidRequestFailure(error) {
4331
4366
  };
4332
4367
  }
4333
4368
  async function tryConfiguredClaudeFallbackChain(args) {
4334
- const { ctx, body, parsedFallbackRequest, fallbackPlan: providedFallbackPlan, modelRouter, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
4369
+ const { ctx, body, getParsedFallbackRequest, fallbackPlan: providedFallbackPlan, modelRouter, tracer, requestStartTime, logProxyBody, logFinalRequest, } = args;
4335
4370
  const fallbackPlan = providedFallbackPlan ??
4336
- buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, modelRouter?.getFallbackChain() ?? [], body.model, parsedFallbackRequest);
4371
+ buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, modelRouter?.getFallbackChain() ?? [], body.model);
4337
4372
  ctx.metadata.fallbackPlan = fallbackPlan.attempts
4338
4373
  .slice(1)
4339
4374
  .map(({ provider, model, reasoningEffort }) => ({
@@ -4445,6 +4480,25 @@ async function tryConfiguredClaudeFallbackChain(args) {
4445
4480
  lastFallbackError = `[${fallback.provider}/${fallback.model}] unavailable: ${reason}`;
4446
4481
  continue;
4447
4482
  }
4483
+ let parsedFallbackRequest;
4484
+ try {
4485
+ parsedFallbackRequest = getParsedFallbackRequest();
4486
+ }
4487
+ catch (parseError) {
4488
+ // A body the translator cannot read fails the same way on every
4489
+ // provider, so report it as the client's error and stop here.
4490
+ const reason = sanitizeForLog(parseError instanceof Error
4491
+ ? parseError.message
4492
+ : String(parseError), 300);
4493
+ terminalFailure = {
4494
+ status: 400,
4495
+ message: `Request could not be translated for provider fallback: ${reason}`,
4496
+ errorType: "invalid_request_error",
4497
+ retryable: false,
4498
+ };
4499
+ lastFallbackError = `[${fallback.provider}/${fallback.model}] untranslatable request: ${reason}`;
4500
+ break;
4501
+ }
4448
4502
  const options = buildProxyFallbackOptions(parsedFallbackRequest, {
4449
4503
  provider: fallback.provider,
4450
4504
  model: fallback.model,
@@ -6615,7 +6669,7 @@ function createClaudeRequestRuntimeContext(args) {
6615
6669
  };
6616
6670
  }
6617
6671
  function createAnthropicAttemptLogger(args) {
6618
- const { ctx, body, toolCount, requestStart, tracer, account, attemptNumber } = args;
6672
+ const { ctx, body, toolCount, requestStart, tracer, account, attemptNumber, transportWaitMs, } = args;
6619
6673
  const attemptStartedAt = Date.now();
6620
6674
  const accountIdentity = resolveRequestLogAccountIdentity(account.label, account.type);
6621
6675
  return (status, errorType, errorMessage, extra) => {
@@ -6651,6 +6705,7 @@ function createAnthropicAttemptLogger(args) {
6651
6705
  responseStatus: status,
6652
6706
  responseTimeMs: attemptCompletedAt - requestStart,
6653
6707
  attemptDurationMs: extra?.attemptDurationMs ?? attemptCompletedAt - attemptStartedAt,
6708
+ ...(transportWaitMs > 0 ? { transportWaitMs } : {}),
6654
6709
  ...(errorType ? { errorType } : {}),
6655
6710
  ...(errorMessage ? { errorMessage } : {}),
6656
6711
  ...(extra?.errorCode ? { errorCode: extra.errorCode } : {}),
@@ -6690,7 +6745,7 @@ async function prepareAnthropicAccountAttempt(args) {
6690
6745
  const { ctx, account, accountState, bodyStr, clientHeaders, isClaudeClientRequest, url, tracer, attemptNumber, currentLastError, currentAuthFailureMessage, logAttempt, logProxyBody: _logProxyBody, } = args;
6691
6746
  let lastError = currentLastError;
6692
6747
  let authFailureMessage = currentAuthFailureMessage;
6693
- if (needsRefresh(account)) {
6748
+ if (needsInlineRefresh(account)) {
6694
6749
  const refreshed = await refreshTokenFromLatest(account, account.persistTarget);
6695
6750
  if (refreshed.success) {
6696
6751
  if (account.persistTarget) {
@@ -7038,7 +7093,10 @@ function buildDeferredClaudeAccountFailureResponse(args) {
7038
7093
  }
7039
7094
  async function handleAnthropicRoutedClaudeRequest(args) {
7040
7095
  const { ctx, body, modelRouter, configGeneration, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), ranking = "expiry-first", preferPrimary = false, sessionAffinityEnabled = false, sessionAffinityIdleTtlMs = 3_600_000, spillInflight = 0, buildLoggedClaudeError, logProxyBody, logFinalRequest, setRoutingDecision, } = args;
7041
- const parsedRequest = parseClaudeRequest(body);
7096
+ // Only the provider fallback legs read the parsed form, and it walks the
7097
+ // whole history, so the native path should not pay for it up front.
7098
+ let parsedRequest;
7099
+ const getParsedRequest = () => (parsedRequest ??= parseClaudeRequest(body));
7042
7100
  // Loop prevention for the Codex-outbound-fallback leg (stage-c-trigger.md
7043
7101
  // §3): a request that reached this handler AS a Codex-outbound fallback
7044
7102
  // loopback call must never be offered a fallback chain that routes back
@@ -7079,7 +7137,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
7079
7137
  }
7080
7138
  const configuredFallbackPlan = buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, isCodexOutboundFallbackLeg
7081
7139
  ? (modelRouter?.getFallbackChain() ?? []).filter((entry) => entry.provider !== "codex")
7082
- : (modelRouter?.getFallbackChain() ?? []), body.model, parsedRequest);
7140
+ : (modelRouter?.getFallbackChain() ?? []), body.model);
7083
7141
  const hasConfiguredFallback = configuredFallbackPlan.attempts
7084
7142
  .slice(1)
7085
7143
  .some((attempt) => Boolean(attempt.provider && attempt.model));
@@ -7111,7 +7169,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
7111
7169
  const configuredFallbackResult = await tryConfiguredClaudeFallbackChain({
7112
7170
  ctx,
7113
7171
  body,
7114
- parsedFallbackRequest: parsedRequest,
7172
+ getParsedFallbackRequest: getParsedRequest,
7115
7173
  fallbackPlan: configuredFallbackPlan,
7116
7174
  modelRouter,
7117
7175
  tracer,
@@ -7258,7 +7316,10 @@ async function handleAnthropicRoutedClaudeRequest(args) {
7258
7316
  let transientSameAccountRetries = 0;
7259
7317
  let rateLimitSameAccountRetries = 0;
7260
7318
  while (true) {
7319
+ const transportWaitStartedAt = performance.now();
7261
7320
  const transportPermit = await providerTransportCoordinator.acquire(ctx.abortSignal);
7321
+ // Rounded so the healthy fast path (microseconds) records nothing.
7322
+ const transportWaitMs = Math.round(performance.now() - transportWaitStartedAt);
7262
7323
  if (transportPermit.allowed === false) {
7263
7324
  loopState.sawNetworkError = true;
7264
7325
  loopState.lastTransportErrorCode =
@@ -7300,6 +7361,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
7300
7361
  tracer,
7301
7362
  account,
7302
7363
  attemptNumber: loopState.attemptNumber,
7364
+ transportWaitMs,
7303
7365
  });
7304
7366
  const preparedAttempt = await prepareAnthropicAccountAttempt({
7305
7367
  ctx,
@@ -7677,7 +7739,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
7677
7739
  const configuredFallbackResult = await tryConfiguredClaudeFallbackChain({
7678
7740
  ctx,
7679
7741
  body,
7680
- parsedFallbackRequest: parsedRequest,
7742
+ getParsedFallbackRequest: getParsedRequest,
7681
7743
  fallbackPlan: configuredFallbackPlan,
7682
7744
  modelRouter,
7683
7745
  tracer,
@@ -8502,10 +8564,28 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
8502
8564
  }, async () => {
8503
8565
  // Single-flight: concurrent full refreshes share one sweep.
8504
8566
  if (!snapshotOnly && !accountFilter) {
8567
+ // Identity is the right key: the runtime config store hands
8568
+ // out one frozen snapshot per config generation, so the same
8569
+ // Set comes back until a reload, which should miss.
8570
+ if (lastLimitsRefresh &&
8571
+ lastLimitsRefresh.accountAllowlist === effectiveAllowlist &&
8572
+ Date.now() - lastLimitsRefresh.completedAt <
8573
+ LIMITS_RESULT_REUSE_MS) {
8574
+ return lastLimitsRefresh.result;
8575
+ }
8505
8576
  if (!limitsRefreshInFlight) {
8506
8577
  limitsRefreshInFlight = refreshAccountLimits({
8507
8578
  accountAllowlist: effectiveAllowlist,
8508
- }).finally(() => {
8579
+ })
8580
+ .then((result) => {
8581
+ lastLimitsRefresh = {
8582
+ completedAt: Date.now(),
8583
+ accountAllowlist: effectiveAllowlist,
8584
+ result,
8585
+ };
8586
+ return result;
8587
+ })
8588
+ .finally(() => {
8509
8589
  limitsRefreshInFlight = null;
8510
8590
  });
8511
8591
  }
@@ -9183,6 +9263,7 @@ export const __testHooks = {
9183
9263
  clearLimitsRefreshStateForTests: () => {
9184
9264
  lastUsageFetchAt.clear();
9185
9265
  limitsRefreshInFlight = null;
9266
+ lastLimitsRefresh = undefined;
9186
9267
  accountQuotaRefreshCoordinator.clear();
9187
9268
  },
9188
9269
  setAccountDirectoryForTests: (override) => {
@@ -45,6 +45,7 @@ import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess
45
45
  import { raceWithAbort } from "../../utils/async/withTimeout.js";
46
46
  import { sanitizeForLog } from "../../utils/logSanitize.js";
47
47
  import { logger } from "../../utils/logger.js";
48
+ import { fetchProxyAware } from "../../proxy/proxyFetch.js";
48
49
  const CODEX_UPSTREAM_TIMEOUT_MS = 15 * 60 * 1000; // 15 min, matches Claude path
49
50
  const DEFAULT_TRANSIENT_COOLDOWN_MS = 60_000;
50
51
  const MAX_TRANSIENT_COOLDOWN_MS = 15 * 60 * 1000;
@@ -1810,7 +1811,7 @@ loopbackPort, internalDispatch) {
1810
1811
  upstreamUrl: CODEX_RESPONSES_URL,
1811
1812
  });
1812
1813
  state.budgetDispatched = true;
1813
- upstream = await fetch(CODEX_RESPONSES_URL, {
1814
+ upstream = await fetchProxyAware(CODEX_RESPONSES_URL, {
1814
1815
  method: "POST",
1815
1816
  headers: upstreamHeaders,
1816
1817
  body: state.bodyStr,
@@ -1990,7 +1991,7 @@ async function handleCodexModelsRequest(ctx) {
1990
1991
  for (;;) {
1991
1992
  let upstream;
1992
1993
  try {
1993
- upstream = await fetch(url, {
1994
+ upstream = await fetchProxyAware(url, {
1994
1995
  method: "GET",
1995
1996
  headers: buildCodexUpstreamHeaders(ctx.headers ?? {}, account),
1996
1997
  // Bound the upstream call, as the responses route does. Without a
@@ -992,6 +992,9 @@ export type RequestAttemptLogEntry = {
992
992
  responseTimeMs: number;
993
993
  /** Time spent in this specific account attempt. */
994
994
  attemptDurationMs?: number;
995
+ /** Time this attempt waited on the shared transport-recovery gate before
996
+ * dispatch. Not part of `attemptDurationMs`; omitted when zero. */
997
+ transportWaitMs?: number;
995
998
  errorType?: string;
996
999
  errorMessage?: string;
997
1000
  /** Low-level transport code such as ETIMEDOUT or EADDRNOTAVAIL. */
@@ -2045,6 +2048,11 @@ export type ProxyEnvironmentSnapshot = {
2045
2048
  socksProxy?: string;
2046
2049
  noProxy?: string;
2047
2050
  };
2051
+ /** Transport deadlines for a proxied upstream request. */
2052
+ export type ProxyTimeouts = {
2053
+ headersTimeout?: number;
2054
+ bodyTimeout?: number;
2055
+ };
2048
2056
  /** Result of a traffic-quiet check. */
2049
2057
  export type QuietStatus = {
2050
2058
  isQuiet: boolean;
@@ -2412,13 +2412,13 @@
2412
2412
  {"objectID":"44f9341fa01636183996f6966d6a698da57d335712c3aa020dfbf6673c3e1e77","title":"Account selection (fill-first)","url":"/docs/features/claude-proxy-troubleshooting#account-selection-fill-first","content":"Account selection (fill-first)","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy Troubleshooting","lvl2":"Account selection (fill-first)","lvl3":""}},
2413
2413
  {"objectID":"cf94af0f8a5039f675a3063e0b06af390d10d2cc678e8ee4b6c3d242cef7dc6a","title":"Exponential backoff progression","url":"/docs/features/claude-proxy-troubleshooting#exponential-backoff-progression","content":"For repeated 429 errors on the same account:\n\n| Backoff Level | Cooldown Duration |\n| ------------- | ----------------- |\n| 0 | 1 second |\n| 1 | 2 seconds |\n| 2 | 4 seconds |\n| 3 | 8 seconds |\n| 4 | 16 seconds |\n| 5 | 32 seconds |\n| ... | ... |\n| Max | 10 minutes (cap) |\n\nThe backoff level resets to zero on a successful request.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy Troubleshooting","lvl2":"Exponential backoff progression","lvl3":""}},
2414
2414
  {"objectID":"464f36b0cd0b7d8db6ad2b4b9618a37434a8270960aa06b24ef167febc873c33","title":"Quick Reference","url":"/docs/features/claude-proxy-troubleshooting#quick-reference","content":"| Symptom | Likely Cause | First Step |\n| ------------------------------- | ------------------------------------ | ----------------------------------------------- |\n| 400 invalid_request_error | Missing cloaking (bare curl) | Use Claude Code, not curl |\n| credit balance too low | API key with no credits in pool | Remove ANTHROPIC_API_KEY or add credits |\n| Extra inputs not permitted | Missing beta headers | Rebuild with pnpm run build:cli |\n| Slow first request (30s) | MCP server init | Verify NEUROLINK_SKIP_MCP=true |\n| Token expired | Auto-refresh missed | neurolink auth login anthropic --method oauth |\n| Refresh rate limited | Too many refresh attempts | Wait 30s, then re-login |\n| Accounts disabled until re-auth | Expired refresh tokens (15 failures) | neurolink auth login anthropic --method oauth |\n| Claude Code bypassing proxy | ANTHROPIC_BASE_URL not set | neurolink proxy setup, restart Claude Code |\n| Raw bytes in stream | Old build with Hono encoding bug | Rebuild with pnpm run build:cli |\n| Tools broken / 0 chunks | MCP tool merging (old build) | Rebuild; verify passthrough mode in logs |\n| No account rotation | Fill-first is working as designed | Check debug logs for 429 + rotation |","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy Troubleshooting","lvl2":"Quick Reference","lvl3":""}},
2415
- {"objectID":"25fe74322f0fa35cad27d8d2c45cc5572e20c3c0295caacca6a0946123f28030","title":"Claude Proxy","url":"/docs/features/claude-proxy","content":"Claude Proxy\n\nNeuroLink includes a Claude-API-compatible proxy server that sits between Claude Code and Anthropic. It pools multiple Claude accounts, handles rate-limit failover automatically, refreshes OAuth tokens on demand before they expire, and falls back to other providers when all Claude accounts are exhausted.\n\nOverview\n\nWhy use the proxy?\n\nClaude Code supports only one Anthropic account at a time. If you hit a rate limit, you wait. If your token expires mid-session, you re-authenticate manually. The NeuroLink proxy solves these problems:\nMulti-account pooling -- Combine multiple Claude Pro/Max subscriptions for higher aggregate throughput.\nAutomatic token refresh -- OAuth tokens are refreshed before they expire (pre-request check + 401 retry).\nRate-limit failover -- When one account hits a 429, the proxy immediately tries the next account with exponential backoff.\nMulti-provider fallback -- When all Claude accounts are exhausted, requests are routed to alternative providers (Gemini, OpenAI, etc.) through NeuroLink's provider layer.\nTransparent to Claude Code -- Set ANTHROPIC_BASE_URL and Claude Code works normally. The proxy auto-configures this on start.\n\nHow it works at a glance\n\nQuick Start\n\nIf you do not already have the CLI installed, install it first:\n\nThen continue with the proxy setup steps below.\n\nOne-command setup\n\nThis command:\nChecks for existing authenticated accounts\nRuns OAuth login if no valid accounts exist\nInstalls the proxy as a launchd service (macOS) that auto-restarts on crash or reboot\nAuto-configures Claude Code to use the proxy\n\nUse --no-service to skip service installation and start the proxy in the foreground instead:\n\nManual setup\n\nRestart the serving worker\n\nrestart starts a replacement worker while the existing worker continues serving.\nThe supervisor switches new connections only after the replacement acknowledges\nreadiness and activation, then lets requests on the previous worker finish.\nYou do not need to run the check separately: restart includes it.\n\nThe command verifies admission, worker identity/version, socket-transfer counters\nand preservation of request-log disk/OTel settings. It does not generate a model\nrequest. A failed candidate has a 120-second readiness limit; the serving worker\nis retained. Concurrent restarts, pending updates and additional restarts while\nan older worker is still draining are refused. A disconnected terminal does not\ncancel the supervisor-owned operation or close admission.\n\nUse --format json for scripts. Exit zero means the check or activation was\nverified. activated_unverified means the replacement activated but a subsequent\ncheck failed; inspect neurolink proxy status before another operation. A lost\ncontrol connection leaves the outcome unknown, rather than triggering a forced\nservice restart.\n\nThe JSON contract is CliProxyRestartOutput: an authenticated supervisor result\nor { \"ok\": false, \"phase\": \"unverified\", \"message\": \"...\" } when the CLI cannot\nreceive or authenticate that result. The latter exits nonzero and deliberately\nomits supervisor/worker fields that could not be verified. It does not mean the\nreplacement failed or was rolled back. Control-server errors after binding are\nreported through the supervisor logger without stopping the serving listener.\n\nThis requires a running supervisor that advertises restart-control support.\nOlder supervisors must first receive a separately planned service activation;\ninstalling a newer CLI alone does not add the capability to an existing process.\nThe command refuses an unsupported supervisor without signalling it.\n\nThe listener and supervisor remain running, so this command does not apply changed\nlaunchd stdout/stderr destinations or replace supervisor/updater code. Those need\na service activation with its own interruption budget. proxy install is service\nsetup, not a routine restart command. The restart command does not rewrite the\nlauncher, environment, routing configuration or update history. Local OTel\ninitialization is checked; collector/backend delivery still needs telemetry\nverification.\n\nHow It Works\n\nRequest Flow\n\nEvery request from Claude Code flows through the proxy in one of two modes:\n\nPassthrough mode (Claude to Claude): The request body is forwarded directly to api.anthropic.com with only the authentication headers modified. This preserves multi-turn conversation history, thinking content, cache control, and tool definitions exactly as Claude Code sent them. No lossy conversion through an intermediate format.\n\nTranslation mode (Claude to other provider): When model routing directs a request to a non-Anthropic provider, the proxy parses the Claude Messages API request into NeuroLink's internal format, calls neurolink.generate() or neurolink.stream(), and serializes the result back into Claude Messages API format (including SSE streaming events). For streaming, the proxy emits SSE keep-alive comments (: keep-alive) every 15 seconds during idle periods to prevent connection","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"","lvl3":""}},
2415
+ {"objectID":"25fe74322f0fa35cad27d8d2c45cc5572e20c3c0295caacca6a0946123f28030","title":"Claude Proxy","url":"/docs/features/claude-proxy","content":"Claude Proxy\n\nNeuroLink includes a Claude-API-compatible proxy server that sits between Claude Code and Anthropic. It pools multiple Claude accounts, handles rate-limit failover automatically, refreshes OAuth tokens on demand before they expire, and falls back to other providers when all Claude accounts are exhausted.\n\nOverview\n\nWhy use the proxy?\n\nClaude Code supports only one Anthropic account at a time. If you hit a rate limit, you wait. If your token expires mid-session, you re-authenticate manually. The NeuroLink proxy solves these problems:\nMulti-account pooling -- Combine multiple Claude Pro/Max subscriptions for higher aggregate throughput.\nAutomatic token refresh -- OAuth tokens are refreshed before they expire (pre-request check + 401 retry).\nRate-limit failover -- When one account hits a 429, the proxy immediately tries the next account with exponential backoff.\nMulti-provider fallback -- When all Claude accounts are exhausted, requests are routed to alternative providers (Gemini, OpenAI, etc.) through NeuroLink's provider layer.\nTransparent to Claude Code -- Set ANTHROPIC_BASE_URL and Claude Code works normally. The proxy auto-configures this on start.\n\nHow it works at a glance\n\nQuick Start\n\nIf you do not already have the CLI installed, install it first:\n\nThen continue with the proxy setup steps below.\n\nOne-command setup\n\nThis command:\nChecks for existing authenticated accounts\nRuns OAuth login if no valid accounts exist\nInstalls the proxy as a launchd service (macOS) that auto-restarts on crash or reboot\nAuto-configures Claude Code to use the proxy\n\nUse --no-service to skip service installation and start the proxy in the foreground instead:\n\nManual setup\n\nFor deployments that require an outbound HTTP proxy, set HTTPS_PROXY and/or\nHTTP_PROXY in the environment of the neurolink proxy process before starting\nit. ALL_PROXY is used when no scheme-specific proxy is set, and NO_PROXY\nbypasses the proxy for matching hosts. These settings cover Anthropic, Codex,\nand Vertex passthrough forwards, OAuth requests, and quota polling. When a\nconfigured proxy fails, these requests fail instead of retrying directly.\n\nRestart the serving worker\n\nrestart starts a replacement worker while the existing worker continues serving.\nThe supervisor switches new connections only after the replacement acknowledges\nreadiness and activation, then lets requests on the previous worker finish.\nYou do not need to run the check separately: restart includes it.\n\nThe command verifies admission, worker identity/version, socket-transfer counters\nand preservation of request-log disk/OTel settings. It does not generate a model\nrequest. A failed candidate has a 120-second readiness limit; the serving worker\nis retained. Concurrent restarts, pending updates and additional restarts while\nan older worker is still draining are refused. A disconnected terminal does not\ncancel the supervisor-owned operation or close admission.\n\nUse --format json for scripts. Exit zero means the check or activation was\nverified. activated_unverified means the replacement activated but a subsequent\ncheck failed; inspect neurolink proxy status before another operation. A lost\ncontrol connection leaves the outcome unknown, rather than triggering a forced\nservice restart.\n\nThe JSON contract is CliProxyRestartOutput: an authenticated supervisor result\nor { \"ok\": false, \"phase\": \"unverified\", \"message\": \"...\" } when the CLI cannot\nreceive or authenticate that result. The latter exits nonzero and deliberately\nomits supervisor/worker fields that could not be verified. It does not mean the\nreplacement failed or was rolled back. Control-server errors after binding are\nreported through the supervisor logger without stopping the serving listener.\n\nThis requires a running supervisor that advertises restart-control support.\nOlder supervisors must first receive a separately planned service activation;\ninstalling a newer CLI alone does not add the capability to an existing process.\nThe command refuses an unsupported supervisor without signalling it.\n\nThe listener and supervisor remain running, so this command does not apply changed\nlaunchd stdout/stderr destinations or replace supervisor/updater code. Those need\na service activation with its own interruption budget. proxy install is service\nsetup, not a routine restart command. The restart command does not rewrite the\nlauncher, environment, routing configuration or update history. Local OTel\ninitialization is checked; collector/backend delivery still needs telemetry\nverification.\n\nHow It Works\n\nRequest Flow\n\nEvery request from Claude Code flows through the proxy in one of two modes:\n\nPassthrough mode (Claude to Claude): The request body is forwarded directly to api.anthropic.com with only the authentication headers modified. This preserves multi-turn conversation history, thinking content, cache control, and tool definitions exactly as Claude Code sent them. No lossy conversion through an intermediate format.\n\nTranslation mod","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"","lvl3":""}},
2416
2416
  {"objectID":"cd6d13f50f433e6c99d05f1b6836b42b1abae54a3e97303ffd5dfcc94117e1f2","title":"Claude Proxy","url":"/docs/features/claude-proxy#claude-proxy","content":"NeuroLink includes a Claude-API-compatible proxy server that sits between Claude Code and Anthropic. It pools multiple Claude accounts, handles rate-limit failover automatically, refreshes OAuth tokens on demand before they expire, and falls back to other providers when all Claude accounts are exhausted.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Claude Proxy","lvl3":""}},
2417
2417
  {"objectID":"00d0e565d5c6b476beb49291259ad3d6d29245039e72ea47d84d223d42fdde68","title":"Why use the proxy?","url":"/docs/features/claude-proxy#why-use-the-proxy","content":"Claude Code supports only one Anthropic account at a time. If you hit a rate limit, you wait. If your token expires mid-session, you re-authenticate manually. The NeuroLink proxy solves these problems:\nMulti-account pooling -- Combine multiple Claude Pro/Max subscriptions for higher aggregate throughput.\nAutomatic token refresh -- OAuth tokens are refreshed before they expire (pre-request check + 401 retry).\nRate-limit failover -- When one account hits a 429, the proxy immediately tries the next account with exponential backoff.\nMulti-provider fallback -- When all Claude accounts are exhausted, requests are routed to alternative providers (Gemini, OpenAI, etc.) through NeuroLink's provider layer.\nTransparent to Claude Code -- Set ANTHROPIC_BASE_URL and Claude Code works normally. The proxy auto-configures this on start.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Why use the proxy?","lvl3":""}},
2418
2418
  {"objectID":"7a61989dbd73cbd8be85bf44e571333710956232acabc0df383e9c0309cb31ff","title":"How it works at a glance","url":"/docs/features/claude-proxy#how-it-works-at-a-glance","content":"How it works at a glance","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"How it works at a glance","lvl3":""}},
2419
2419
  {"objectID":"4fff8ce3b2980c189940fe8f3dbd7fb99c961dd3e73af086a911631f0c0f6186","title":"Quick Start","url":"/docs/features/claude-proxy#quick-start","content":"If you do not already have the CLI installed, install it first:\n\nThen continue with the proxy setup steps below.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Quick Start","lvl3":""}},
2420
2420
  {"objectID":"ec115266d6030cdba9743aaa9f04d18cfcce5b0e7a0bfa777bf0b8d6ca51d764","title":"One-command setup","url":"/docs/features/claude-proxy#one-command-setup","content":"This command:\nChecks for existing authenticated accounts\nRuns OAuth login if no valid accounts exist\nInstalls the proxy as a launchd service (macOS) that auto-restarts on crash or reboot\nAuto-configures Claude Code to use the proxy\n\nUse --no-service to skip service installation and start the proxy in the foreground instead:","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"One-command setup","lvl3":""}},
2421
- {"objectID":"485846ce8d40e93dd7e3ada58798719e3694cbf411b4d41571c075d887900eb3","title":"Manual setup","url":"/docs/features/claude-proxy#manual-setup","content":"Manual setup","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Manual setup","lvl3":""}},
2421
+ {"objectID":"485846ce8d40e93dd7e3ada58798719e3694cbf411b4d41571c075d887900eb3","title":"Manual setup","url":"/docs/features/claude-proxy#manual-setup","content":"For deployments that require an outbound HTTP proxy, set HTTPS_PROXY and/or\nHTTP_PROXY in the environment of the neurolink proxy process before starting\nit. ALL_PROXY is used when no scheme-specific proxy is set, and NO_PROXY\nbypasses the proxy for matching hosts. These settings cover Anthropic, Codex,\nand Vertex passthrough forwards, OAuth requests, and quota polling. When a\nconfigured proxy fails, these requests fail instead of retrying directly.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Manual setup","lvl3":""}},
2422
2422
  {"objectID":"8c067e169758adc810089db64e417d6dc960a750ae8c038d6737eb45306fd9d8","title":"Restart the serving worker","url":"/docs/features/claude-proxy#restart-the-serving-worker","content":"restart starts a replacement worker while the existing worker continues serving.\nThe supervisor switches new connections only after the replacement acknowledges\nreadiness and activation, then lets requests on the previous worker finish.\nYou do not need to run the check separately: restart includes it.\n\nThe command verifies admission, worker identity/version, socket-transfer counters\nand preservation of request-log disk/OTel settings. It does not generate a model\nrequest. A failed candidate has a 120-second readiness limit; the serving worker\nis retained. Concurrent restarts, pending updates and additional restarts while\nan older worker is still draining are refused. A disconnected terminal does not\ncancel the supervisor-owned operation or close admission.\n\nUse --format json for scripts. Exit zero means the check or activation was\nverified. activated_unverified means the replacement activated but a subsequent\ncheck failed; inspect neurolink proxy status before another operation. A lost\ncontrol connection leaves the outcome unknown, rather than triggering a forced\nservice restart.\n\nThe JSON contract is CliProxyRestartOutput: an authenticated supervisor result\nor { \"ok\": false, \"phase\": \"unverified\", \"message\": \"...\" } when the CLI cannot\nreceive or authenticate that result. The latter exits nonzero and deliberately\nomits supervisor/worker fields that could not be verified. It does not mean the\nreplacement failed or was rolled back. Control-server errors after binding are\nreported through the supervisor logger without stopping the serving listener.\n\nThis requires a running supervisor that advertises restart-control support.\nOlder supervisors must first receive a separately planned service activation;\ninstalling a newer CLI alone does not add the capability to an existing process.\nThe command refuses an unsupported supervisor without signalling it.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Restart the serving worker","lvl3":""}},
2423
2423
  {"objectID":"d3f2f492ba79c35c567408642c86e730a966688e4993622fd4d81190772246e8","title":"Request Flow","url":"/docs/features/claude-proxy#request-flow","content":"Every request from Claude Code flows through the proxy in one of two modes:\n\nPassthrough mode (Claude to Claude): The request body is forwarded directly to api.anthropic.com with only the authentication headers modified. This preserves multi-turn conversation history, thinking content, cache control, and tool definitions exactly as Claude Code sent them. No lossy conversion through an intermediate format.\n\nTranslation mode (Claude to other provider): When model routing directs a request to a non-Anthropic provider, the proxy parses the Claude Messages API request into NeuroLink's internal format, calls neurolink.generate() or neurolink.stream(), and serializes the result back into Claude Messages API format (including SSE streaming events). For streaming, the proxy emits SSE keep-alive comments (: keep-alive) every 15 seconds during idle periods to prevent connection timeouts.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Request Flow","lvl3":""}},
2424
2424
  {"objectID":"d5e5f137807efbff5800106aa9777b782e2a973ddf98f31877f1f139acd785aa","title":"Trace And Session Context","url":"/docs/features/claude-proxy#trace-and-session-context","content":"If the caller sends W3C trace headers (traceparent, tracestate) or NeuroLink session headers (x-neurolink-session-id, x-neurolink-user-id, x-neurolink-conversation-id), the proxy links its spans to the caller trace and preserves that session/user/conversation context in proxy traces and logs.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Trace And Session Context","lvl3":""}},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "13.8.5",
3
+ "version": "13.8.7",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
6
6
  "author": {
@@ -230,6 +230,7 @@
230
230
  "test:local-usage": "pnpm exec tsx test/continuous-test-suite-local-usage.ts",
231
231
  "test:dynamic": "pnpm exec tsx test/continuous-test-suite-dynamic.ts",
232
232
  "test:proxy": "pnpm exec tsx test/continuous-test-suite-proxy.ts",
233
+ "test:proxy-egress": "pnpm exec tsx test/continuous-test-suite-proxy-egress.ts",
233
234
  "test:proxy-connect-retry": "pnpm exec tsx test/continuous-test-suite-proxy-connect-retry.ts",
234
235
  "test:proxy-reliability": "node scripts/run-proxy-reliability.mjs",
235
236
  "test:proxy-telemetry": "pnpm exec tsx test/continuous-test-suite-proxy-telemetry.ts",