@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.
- package/CHANGELOG.md +7 -2
- package/dist/auth/anthropicOAuth.js +7 -6
- package/dist/auth/codexOAuth.js +2 -1
- package/dist/browser/neurolink.min.js +487 -487
- package/dist/cli/commands/proxy.js +26 -4
- package/dist/mcp/mcpClientFactory.d.ts +12 -1
- package/dist/mcp/mcpClientFactory.js +87 -8
- package/dist/providers/anthropic/client.js +2 -2
- package/dist/proxy/accountUsage.js +2 -1
- package/dist/proxy/codexAccountUsage.js +2 -1
- package/dist/proxy/providerTransportCoordinator.d.ts +6 -0
- package/dist/proxy/providerTransportCoordinator.js +34 -1
- package/dist/proxy/proxyDispatcher.js +2 -2
- package/dist/proxy/proxyFetch.d.ts +9 -3
- package/dist/proxy/proxyFetch.js +31 -9
- package/dist/proxy/routingPolicy.d.ts +1 -1
- package/dist/proxy/snapshotPersistence.d.ts +6 -0
- package/dist/proxy/snapshotPersistence.js +10 -0
- package/dist/proxy/tokenRefresh.d.ts +2 -0
- package/dist/proxy/tokenRefresh.js +19 -3
- package/dist/proxy/vertexAnthropicFallback.js +2 -2
- package/dist/server/routes/claudeProxyRoutes.js +107 -26
- package/dist/server/routes/codexProxyRoutes.js +3 -2
- package/dist/types/proxy.d.ts +8 -0
- package/docs-site/static/search-index.json +2 -2
- package/package.json +2 -1
|
@@ -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,
|
|
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
|
|
296
|
+
const response = await fetchProxyAware(url, {
|
|
269
297
|
...init,
|
|
270
298
|
body: dispatchedBody,
|
|
271
|
-
|
|
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
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
|
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
|
|
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
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -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
|
|
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":"
|
|
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.
|
|
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",
|