@juspay/neurolink 12.9.3 → 12.9.5
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 +2 -2
- package/dist/auth/codexOAuth.d.ts +1 -1
- package/dist/auth/codexOAuth.js +3 -1
- package/dist/browser/neurolink.min.js +285 -285
- package/dist/providers/catalog/cloudflare.json +3 -3
- package/dist/proxy/codexAccountUsage.js +22 -4
- package/dist/proxy/providerTransportCoordinator.d.ts +4 -1
- package/dist/proxy/providerTransportCoordinator.js +8 -1
- package/dist/server/routes/claudeProxyRoutes.d.ts +17 -0
- package/dist/server/routes/claudeProxyRoutes.js +76 -4
- package/dist/types/codex.d.ts +10 -1
- package/dist/types/proxy.d.ts +9 -0
- package/package.json +2 -1
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"contextWindow": 24000,
|
|
48
48
|
"vision": false,
|
|
49
49
|
"status": "production",
|
|
50
|
-
"description": "Llama 3.2 11B Vision Instruct —
|
|
50
|
+
"description": "Llama 3.2 11B Vision Instruct — Meta Community License accepted on the account 2026-09-02, but Cloudflare's OpenAI-compatible endpoint accepts no image input for it: an image content part is rejected (400) and a top-level image is ignored ('There is no image'). The model reads images only through the native /ai/run prompt+image shape, which this OpenAI-compatible provider does not speak, so vision stays false here"
|
|
51
51
|
},
|
|
52
52
|
"@cf/mistral/mistral-7b-instruct-v0.2": {
|
|
53
53
|
"enumMember": "MISTRAL_7B_INSTRUCT_V0_2",
|
|
@@ -114,8 +114,8 @@
|
|
|
114
114
|
"method": "real chat call per id against the live account"
|
|
115
115
|
},
|
|
116
116
|
"liveMatrix": {
|
|
117
|
-
"date": "2026-
|
|
118
|
-
"result": "Live tool round-trip verified on @cf/meta/llama-3.1-8b-instruct-fast, both non-streaming and streaming, once messageContentFormat normalization was applied; before it the follow-up turn was rejected with HTTP 400 (content sent as array/null). Of the three ids that failed, only two are actually gone: mistral-7b-instruct-v0.2 ('No such model') and qwen1.5-14b-chat-awq ('deprecated on 2025-10-01'). llama-3.2-11b-vision-instruct is alive but gated behind a one-time Community License acceptance on the account, so it answers 403 and is left production with vision false rather than retired."
|
|
117
|
+
"date": "2026-09-02",
|
|
118
|
+
"result": "Live tool round-trip verified on @cf/meta/llama-3.1-8b-instruct-fast, both non-streaming and streaming, once messageContentFormat normalization was applied; before it the follow-up turn was rejected with HTTP 400 (content sent as array/null). Of the three ids that failed, only two are actually gone: mistral-7b-instruct-v0.2 ('No such model') and qwen1.5-14b-chat-awq ('deprecated on 2025-10-01'). llama-3.2-11b-vision-instruct is alive but gated behind a one-time Community License acceptance on the account, so it answers 403 and is left production with vision false rather than retired. Update 2026-09-02: the Community License was accepted on the account; plain chat on llama-3.2-11b-vision-instruct now returns 200 on both endpoints, and the native /ai/run prompt+image call describes a 64x64 test image correctly. The OpenAI-compatible endpoint still cannot carry an image (content-part array → 400, top-level image ignored), so the vision flag remains false for this provider."
|
|
119
119
|
},
|
|
120
120
|
"addedInPR": "https://github.com/juspay/neurolink/pull/1587"
|
|
121
121
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { tokenStore } from "../auth/tokenStore.js";
|
|
10
10
|
import { normalizeAnthropicAccountKey } from "./accountSelection.js";
|
|
11
|
-
import {
|
|
11
|
+
import { CODEX_USAGE_URL, CODEX_USER_AGENT, decodeCodexAccessToken, resolveCodexAccountId, } from "../auth/codexOAuth.js";
|
|
12
12
|
import { logger } from "../utils/logger.js";
|
|
13
13
|
export const CODEX_ACCOUNT_PREFIX = "codex:";
|
|
14
14
|
/** Keep an account label in the Codex namespace used by its token store. */
|
|
@@ -71,6 +71,7 @@ function toFraction(percent) {
|
|
|
71
71
|
// Codex reports 0-100; clamp and normalise to 0-1.
|
|
72
72
|
return Math.min(1, Math.max(0, percent / 100));
|
|
73
73
|
}
|
|
74
|
+
/** Convert absolute or relative Codex window resets into epoch seconds. */
|
|
74
75
|
function windowResetEpochSeconds(window, nowSeconds) {
|
|
75
76
|
if (!window) {
|
|
76
77
|
return 0;
|
|
@@ -83,9 +84,18 @@ function windowResetEpochSeconds(window, nowSeconds) {
|
|
|
83
84
|
? Math.floor(window.resets_at / 1000)
|
|
84
85
|
: window.resets_at;
|
|
85
86
|
}
|
|
87
|
+
if (typeof window.reset_at === "number" &&
|
|
88
|
+
Number.isFinite(window.reset_at) &&
|
|
89
|
+
window.reset_at > 0) {
|
|
90
|
+
return window.reset_at > 4_102_444_800
|
|
91
|
+
? Math.floor(window.reset_at / 1000)
|
|
92
|
+
: window.reset_at;
|
|
93
|
+
}
|
|
86
94
|
const relative = typeof window.resets_in_seconds === "number"
|
|
87
95
|
? window.resets_in_seconds
|
|
88
|
-
: window.
|
|
96
|
+
: typeof window.reset_after_seconds === "number"
|
|
97
|
+
? window.reset_after_seconds
|
|
98
|
+
: window.reset_after;
|
|
89
99
|
if (typeof relative === "number" &&
|
|
90
100
|
Number.isFinite(relative) &&
|
|
91
101
|
relative > 0) {
|
|
@@ -177,7 +187,6 @@ export async function fetchCodexAccountUsage(account, options = {}) {
|
|
|
177
187
|
headers: {
|
|
178
188
|
Authorization: `Bearer ${account.token}`,
|
|
179
189
|
...(accountId ? { "chatgpt-account-id": accountId } : {}),
|
|
180
|
-
originator: CODEX_ORIGINATOR,
|
|
181
190
|
"User-Agent": CODEX_USER_AGENT,
|
|
182
191
|
Accept: "application/json",
|
|
183
192
|
},
|
|
@@ -186,11 +195,20 @@ export async function fetchCodexAccountUsage(account, options = {}) {
|
|
|
186
195
|
if (response.status === 401 || response.status === 403) {
|
|
187
196
|
return { ok: false, reason: "auth" };
|
|
188
197
|
}
|
|
198
|
+
if (response.status === 429) {
|
|
199
|
+
return { ok: false, reason: "rate_limited" };
|
|
200
|
+
}
|
|
189
201
|
if (!response.ok) {
|
|
190
202
|
return { ok: false, reason: "http" };
|
|
191
203
|
}
|
|
192
204
|
const usage = (await response.json());
|
|
193
|
-
|
|
205
|
+
const rateLimits = usage.rate_limit
|
|
206
|
+
? {
|
|
207
|
+
primary: usage.rate_limit.primary_window,
|
|
208
|
+
secondary: usage.rate_limit.secondary_window,
|
|
209
|
+
}
|
|
210
|
+
: usage.rate_limits;
|
|
211
|
+
return { ok: true, quota: codexRateLimitsToQuota(rateLimits) };
|
|
194
212
|
}
|
|
195
213
|
catch (error) {
|
|
196
214
|
logger.debug(`Codex usage fetch failed for ${account.label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5,10 +5,13 @@ export declare class ProviderTransportCoordinator {
|
|
|
5
5
|
private backoffUntil;
|
|
6
6
|
private lastErrorCode;
|
|
7
7
|
private lastTransportScope;
|
|
8
|
+
/** Whether the failure that degraded the transport happened before any
|
|
9
|
+
* request byte was sent. Waiters use it to pick their retry budget. */
|
|
10
|
+
private lastFailureConnectPhase;
|
|
8
11
|
private probe;
|
|
9
12
|
acquire(signal?: AbortSignal): Promise<ProxyProviderTransportPermit>;
|
|
10
13
|
reportSuccess(permit: ProxyProviderTransportPermit): void;
|
|
11
|
-
reportTransportFailure(errorCode: string | undefined, transportScope: "shared_provider_transport" | "connection_transport", permit: ProxyProviderTransportPermit): void;
|
|
14
|
+
reportTransportFailure(errorCode: string | undefined, transportScope: "shared_provider_transport" | "connection_transport", permit: ProxyProviderTransportPermit, connectPhase?: boolean): void;
|
|
12
15
|
reportProbeAbandoned(permit: ProxyProviderTransportPermit): void;
|
|
13
16
|
clear(): void;
|
|
14
17
|
private wait;
|
|
@@ -7,6 +7,9 @@ export class ProviderTransportCoordinator {
|
|
|
7
7
|
backoffUntil = 0;
|
|
8
8
|
lastErrorCode = null;
|
|
9
9
|
lastTransportScope = "shared_provider_transport";
|
|
10
|
+
/** Whether the failure that degraded the transport happened before any
|
|
11
|
+
* request byte was sent. Waiters use it to pick their retry budget. */
|
|
12
|
+
lastFailureConnectPhase = false;
|
|
10
13
|
probe;
|
|
11
14
|
async acquire(signal) {
|
|
12
15
|
if (!this.degraded) {
|
|
@@ -31,6 +34,7 @@ export class ProviderTransportCoordinator {
|
|
|
31
34
|
allowed: false,
|
|
32
35
|
errorCode: this.lastErrorCode,
|
|
33
36
|
transportScope: this.lastTransportScope,
|
|
37
|
+
connectPhase: this.lastFailureConnectPhase,
|
|
34
38
|
};
|
|
35
39
|
}
|
|
36
40
|
let resolveProbe;
|
|
@@ -55,12 +59,13 @@ export class ProviderTransportCoordinator {
|
|
|
55
59
|
this.backoffUntil = 0;
|
|
56
60
|
this.lastErrorCode = null;
|
|
57
61
|
this.lastTransportScope = "shared_provider_transport";
|
|
62
|
+
this.lastFailureConnectPhase = false;
|
|
58
63
|
if (permit.probe && this.probe?.generation === permit.generation) {
|
|
59
64
|
this.probe.resolve("recovered");
|
|
60
65
|
this.probe = undefined;
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
|
-
reportTransportFailure(errorCode, transportScope, permit) {
|
|
68
|
+
reportTransportFailure(errorCode, transportScope, permit, connectPhase = false) {
|
|
64
69
|
if (!permit.allowed || permit.generation !== this.generation) {
|
|
65
70
|
return;
|
|
66
71
|
}
|
|
@@ -71,6 +76,7 @@ export class ProviderTransportCoordinator {
|
|
|
71
76
|
this.backoffUntil = Date.now() + SHARED_TRANSPORT_BACKOFF_MS;
|
|
72
77
|
this.lastErrorCode = errorCode ?? null;
|
|
73
78
|
this.lastTransportScope = transportScope;
|
|
79
|
+
this.lastFailureConnectPhase = connectPhase;
|
|
74
80
|
if (permit.probe && this.probe?.generation === permit.generation) {
|
|
75
81
|
this.probe.resolve("failed");
|
|
76
82
|
this.probe = undefined;
|
|
@@ -96,6 +102,7 @@ export class ProviderTransportCoordinator {
|
|
|
96
102
|
this.backoffUntil = 0;
|
|
97
103
|
this.lastErrorCode = null;
|
|
98
104
|
this.lastTransportScope = "shared_provider_transport";
|
|
105
|
+
this.lastFailureConnectPhase = false;
|
|
99
106
|
}
|
|
100
107
|
async wait(ms, signal) {
|
|
101
108
|
if (signal?.aborted) {
|
|
@@ -389,6 +389,20 @@ declare function reconcileEligibleAccountRuntimeState(account: ProxyPassthroughA
|
|
|
389
389
|
export declare function getTransientSameAccountRetryDelayMs(retryNumber: number): number;
|
|
390
390
|
export declare function getOverloadRotationDelayMs(attemptNumber: number): number;
|
|
391
391
|
declare function describeTransportError(error: unknown): string;
|
|
392
|
+
/**
|
|
393
|
+
* Whether a transport failure provably happened while connecting, before any
|
|
394
|
+
* byte of the request left the process.
|
|
395
|
+
*
|
|
396
|
+
* Node tags its connect errors with `syscall: "connect"`; with happy-eyeballs
|
|
397
|
+
* enabled (the default) the error is an AggregateError whose every entry is
|
|
398
|
+
* one such attempt, and undici wraps either as `fetch failed` with the
|
|
399
|
+
* original as `cause`. The code alone cannot make this call: an `ETIMEDOUT`
|
|
400
|
+
* from the connect timer and an `ETIMEDOUT` from a socket that went quiet
|
|
401
|
+
* after dispatch look identical by code, and only the first is safe to
|
|
402
|
+
* retry. Both proxies behind one lossy Wi-Fi uplink returned 116 terminal
|
|
403
|
+
* 502s in a day on exactly that ambiguity.
|
|
404
|
+
*/
|
|
405
|
+
declare function isConnectPhaseNetworkError(error: unknown): boolean;
|
|
392
406
|
/**
|
|
393
407
|
* Determine whether a POST can be retried without risking duplicate provider
|
|
394
408
|
* work. Only failures that prove connection establishment did not complete are
|
|
@@ -470,6 +484,9 @@ export declare const __testHooks: {
|
|
|
470
484
|
clearLimitsRefreshStateForTests: () => void;
|
|
471
485
|
setAccountDirectoryForTests: (override: ProxyAccountDirectoryOverride | null) => void;
|
|
472
486
|
isRetryableNetworkError: typeof isRetryableNetworkError;
|
|
487
|
+
isConnectPhaseNetworkError: typeof isConnectPhaseNetworkError;
|
|
488
|
+
MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES: number;
|
|
489
|
+
clearProviderTransportCoordinatorForTests: () => void;
|
|
473
490
|
isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
|
|
474
491
|
getStreamFailureDetails: typeof getStreamFailureDetails;
|
|
475
492
|
trackUpstreamReadableStream: typeof trackUpstreamReadableStream;
|
|
@@ -109,6 +109,15 @@ let lastKnownAccountCount = 0;
|
|
|
109
109
|
const MAX_AUTH_RETRIES = 5;
|
|
110
110
|
const MAX_TRANSIENT_SAME_ACCOUNT_RETRIES = 2;
|
|
111
111
|
const TRANSIENT_SAME_ACCOUNT_RETRY_DELAYS_MS = [250, 1_000];
|
|
112
|
+
/**
|
|
113
|
+
* Retry budget for failures that happened before any request byte was sent
|
|
114
|
+
* (a SYN lost on a lossy uplink). Nothing was dispatched, so each retry is
|
|
115
|
+
* free of duplicate-work risk; on a link losing one connect in five, four
|
|
116
|
+
* retries take the per-request failure rate from about 20% to under 1%.
|
|
117
|
+
* Delays follow TRANSIENT_SAME_ACCOUNT_RETRY_DELAYS_MS, clamped to its last
|
|
118
|
+
* entry.
|
|
119
|
+
*/
|
|
120
|
+
const MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES = 4;
|
|
112
121
|
const OVERLOAD_ACCOUNT_ROTATION_DELAYS_MS = [250, 500, 1_000, 2_000];
|
|
113
122
|
const MAX_FALLBACK_NETWORK_RETRIES = 1;
|
|
114
123
|
const FALLBACK_STREAM_IDLE_TIMEOUT_MS = 2 * 60 * 1000;
|
|
@@ -5841,6 +5850,9 @@ function createAnthropicAttemptLogger(args) {
|
|
|
5841
5850
|
? { cacheReadTokens: extra.cacheReadTokens }
|
|
5842
5851
|
: {}),
|
|
5843
5852
|
...(extra?.retryable !== undefined ? { retryable: extra.retryable } : {}),
|
|
5853
|
+
...(extra?.connectPhase !== undefined
|
|
5854
|
+
? { connectPhase: extra.connectPhase }
|
|
5855
|
+
: {}),
|
|
5844
5856
|
...(extra?.rateLimitKind ? { rateLimitKind: extra.rateLimitKind } : {}),
|
|
5845
5857
|
...(extra?.cooldownReason
|
|
5846
5858
|
? { cooldownReason: extra.cooldownReason }
|
|
@@ -6044,6 +6056,7 @@ async function fetchAnthropicAccountResponse(args) {
|
|
|
6044
6056
|
}
|
|
6045
6057
|
catch (fetchErr) {
|
|
6046
6058
|
const retryable = isRetryableNetworkError(fetchErr);
|
|
6059
|
+
const connectPhase = isConnectPhaseNetworkError(fetchErr);
|
|
6047
6060
|
const transportScope = classifyNetworkTransportScope(fetchErr);
|
|
6048
6061
|
// Every dispatched upstream request is an attempt, including terminal
|
|
6049
6062
|
// transport failures. Record it once before preserving the throw behavior.
|
|
@@ -6055,6 +6068,7 @@ async function fetchAnthropicAccountResponse(args) {
|
|
|
6055
6068
|
logger.always(`[proxy] fetch error account=${account.label} code=${errorCode} (${retryable ? "retryable" : "terminal"}): ${errorMessage}`);
|
|
6056
6069
|
logAttempt(502, "network_error", errorMessage, {
|
|
6057
6070
|
retryable,
|
|
6071
|
+
connectPhase,
|
|
6058
6072
|
errorCode,
|
|
6059
6073
|
transportScope,
|
|
6060
6074
|
});
|
|
@@ -6071,6 +6085,7 @@ async function fetchAnthropicAccountResponse(args) {
|
|
|
6071
6085
|
retrySameAccount: true,
|
|
6072
6086
|
transportScope,
|
|
6073
6087
|
errorCode,
|
|
6088
|
+
connectPhase,
|
|
6074
6089
|
lastError,
|
|
6075
6090
|
sawRateLimit,
|
|
6076
6091
|
sawNetworkError,
|
|
@@ -6340,6 +6355,20 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
6340
6355
|
transportPermit.errorCode ?? undefined;
|
|
6341
6356
|
loopState.lastTransportScope = transportPermit.transportScope;
|
|
6342
6357
|
loopState.lastError = `Anthropic transport recovery probe failed (${transportPermit.errorCode ?? "unknown"})`;
|
|
6358
|
+
// The probe that failed was another request's attempt; this one has
|
|
6359
|
+
// sent nothing. Give it the same bounded same-account budget a direct
|
|
6360
|
+
// transport failure gets, instead of failing every queued request the
|
|
6361
|
+
// moment one probe times out.
|
|
6362
|
+
const probeRetryBudget = transportPermit.connectPhase
|
|
6363
|
+
? MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES
|
|
6364
|
+
: MAX_TRANSIENT_SAME_ACCOUNT_RETRIES;
|
|
6365
|
+
if (transientSameAccountRetries < probeRetryBudget) {
|
|
6366
|
+
transientSameAccountRetries += 1;
|
|
6367
|
+
const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
|
|
6368
|
+
logger.always(`[proxy] retrying same account=${account.label} after failed transport recovery probe (${transientSameAccountRetries}/${probeRetryBudget}) in ${delayMs}ms`);
|
|
6369
|
+
await sleep(delayMs);
|
|
6370
|
+
continue;
|
|
6371
|
+
}
|
|
6343
6372
|
break accountLoop;
|
|
6344
6373
|
}
|
|
6345
6374
|
loopState.attemptNumber += 1;
|
|
@@ -6441,7 +6470,7 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
6441
6470
|
throw error;
|
|
6442
6471
|
}
|
|
6443
6472
|
if (fetchResult.transportScope) {
|
|
6444
|
-
providerTransportCoordinator.reportTransportFailure(fetchResult.errorCode, fetchResult.transportScope, transportPermit);
|
|
6473
|
+
providerTransportCoordinator.reportTransportFailure(fetchResult.errorCode, fetchResult.transportScope, transportPermit, fetchResult.connectPhase === true);
|
|
6445
6474
|
}
|
|
6446
6475
|
else {
|
|
6447
6476
|
providerTransportCoordinator.reportSuccess(transportPermit);
|
|
@@ -6518,12 +6547,16 @@ async function handleAnthropicRoutedClaudeRequest(args) {
|
|
|
6518
6547
|
}
|
|
6519
6548
|
continue accountLoop;
|
|
6520
6549
|
}
|
|
6521
|
-
// Transient error retry (network errors, 529 overloaded)
|
|
6550
|
+
// Transient error retry (network errors, 529 overloaded). A failure
|
|
6551
|
+
// from the connect phase sent nothing, so it earns the larger budget.
|
|
6552
|
+
const sameAccountRetryBudget = fetchResult.connectPhase
|
|
6553
|
+
? MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES
|
|
6554
|
+
: MAX_TRANSIENT_SAME_ACCOUNT_RETRIES;
|
|
6522
6555
|
if (fetchResult.retrySameAccount &&
|
|
6523
|
-
transientSameAccountRetries <
|
|
6556
|
+
transientSameAccountRetries < sameAccountRetryBudget) {
|
|
6524
6557
|
transientSameAccountRetries += 1;
|
|
6525
6558
|
const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
|
|
6526
|
-
logger.always(`[proxy] retrying same account=${account.label} after transient network error (${transientSameAccountRetries}/${
|
|
6559
|
+
logger.always(`[proxy] retrying same account=${account.label} after transient network error (${transientSameAccountRetries}/${sameAccountRetryBudget}) in ${delayMs}ms`);
|
|
6527
6560
|
await sleep(delayMs);
|
|
6528
6561
|
continue;
|
|
6529
6562
|
}
|
|
@@ -7883,12 +7916,46 @@ function describeTransportError(error) {
|
|
|
7883
7916
|
.join(": ");
|
|
7884
7917
|
return detail ? `${message} (${detail})` : message;
|
|
7885
7918
|
}
|
|
7919
|
+
/**
|
|
7920
|
+
* Whether a transport failure provably happened while connecting, before any
|
|
7921
|
+
* byte of the request left the process.
|
|
7922
|
+
*
|
|
7923
|
+
* Node tags its connect errors with `syscall: "connect"`; with happy-eyeballs
|
|
7924
|
+
* enabled (the default) the error is an AggregateError whose every entry is
|
|
7925
|
+
* one such attempt, and undici wraps either as `fetch failed` with the
|
|
7926
|
+
* original as `cause`. The code alone cannot make this call: an `ETIMEDOUT`
|
|
7927
|
+
* from the connect timer and an `ETIMEDOUT` from a socket that went quiet
|
|
7928
|
+
* after dispatch look identical by code, and only the first is safe to
|
|
7929
|
+
* retry. Both proxies behind one lossy Wi-Fi uplink returned 116 terminal
|
|
7930
|
+
* 502s in a day on exactly that ambiguity.
|
|
7931
|
+
*/
|
|
7932
|
+
function isConnectPhaseNetworkError(error) {
|
|
7933
|
+
const seen = new Set();
|
|
7934
|
+
let current = error;
|
|
7935
|
+
while (current && typeof current === "object" && !seen.has(current)) {
|
|
7936
|
+
seen.add(current);
|
|
7937
|
+
const candidate = current;
|
|
7938
|
+
if (candidate.syscall === "connect") {
|
|
7939
|
+
return true;
|
|
7940
|
+
}
|
|
7941
|
+
if (Array.isArray(candidate.errors) && candidate.errors.length > 0) {
|
|
7942
|
+
return candidate.errors.every((entry) => entry !== null &&
|
|
7943
|
+
typeof entry === "object" &&
|
|
7944
|
+
entry.syscall === "connect");
|
|
7945
|
+
}
|
|
7946
|
+
current = candidate.cause;
|
|
7947
|
+
}
|
|
7948
|
+
return false;
|
|
7949
|
+
}
|
|
7886
7950
|
/**
|
|
7887
7951
|
* Determine whether a POST can be retried without risking duplicate provider
|
|
7888
7952
|
* work. Only failures that prove connection establishment did not complete are
|
|
7889
7953
|
* safe; a reset, socket error, or response timeout can happen after dispatch.
|
|
7890
7954
|
*/
|
|
7891
7955
|
function isRetryableNetworkError(error) {
|
|
7956
|
+
if (isConnectPhaseNetworkError(error)) {
|
|
7957
|
+
return true;
|
|
7958
|
+
}
|
|
7892
7959
|
const code = getErrorCode(error);
|
|
7893
7960
|
return (code !== undefined &&
|
|
7894
7961
|
[
|
|
@@ -8112,6 +8179,11 @@ export const __testHooks = {
|
|
|
8112
8179
|
accountDirectoryOverride = override;
|
|
8113
8180
|
},
|
|
8114
8181
|
isRetryableNetworkError,
|
|
8182
|
+
isConnectPhaseNetworkError,
|
|
8183
|
+
MAX_CONNECT_PHASE_SAME_ACCOUNT_RETRIES,
|
|
8184
|
+
clearProviderTransportCoordinatorForTests: () => {
|
|
8185
|
+
providerTransportCoordinator.clear();
|
|
8186
|
+
},
|
|
8115
8187
|
isPermanentRefreshFailure,
|
|
8116
8188
|
getStreamFailureDetails,
|
|
8117
8189
|
trackUpstreamReadableStream,
|
package/dist/types/codex.d.ts
CHANGED
|
@@ -58,6 +58,9 @@ export type CodexRateLimitWindow = {
|
|
|
58
58
|
* instead of degrading to the transient ceiling. */
|
|
59
59
|
reset_after?: number | null;
|
|
60
60
|
resets_at?: number | null;
|
|
61
|
+
/** Current WHAM usage fields. */
|
|
62
|
+
reset_after_seconds?: number | null;
|
|
63
|
+
reset_at?: number | null;
|
|
61
64
|
};
|
|
62
65
|
/** Codex rate-limit block: a primary (short) and secondary (long) window. */
|
|
63
66
|
export type CodexRateLimits = {
|
|
@@ -66,7 +69,13 @@ export type CodexRateLimits = {
|
|
|
66
69
|
};
|
|
67
70
|
/** Loose shape of the Codex usage endpoint response. */
|
|
68
71
|
export type CodexUsageResponse = {
|
|
72
|
+
/** Legacy Codex usage payload. */
|
|
69
73
|
rate_limits?: CodexRateLimits | null;
|
|
74
|
+
/** Current ChatGPT WHAM account-usage payload. */
|
|
75
|
+
rate_limit?: {
|
|
76
|
+
primary_window?: CodexRateLimitWindow | null;
|
|
77
|
+
secondary_window?: CodexRateLimitWindow | null;
|
|
78
|
+
} | null;
|
|
70
79
|
plan_type?: string | null;
|
|
71
80
|
};
|
|
72
81
|
/** Result of a single Codex usage fetch. */
|
|
@@ -75,7 +84,7 @@ export type CodexUsageFetchResult = {
|
|
|
75
84
|
quota: AccountQuota;
|
|
76
85
|
} | {
|
|
77
86
|
ok: false;
|
|
78
|
-
reason: "not_oauth" | "auth" | "http" | "network" | "parse";
|
|
87
|
+
reason: "not_oauth" | "auth" | "rate_limited" | "http" | "network" | "parse";
|
|
79
88
|
};
|
|
80
89
|
/** A Codex account with its runtime cooldown/quota state hydrated from disk. */
|
|
81
90
|
export type CodexRuntimeAccount = {
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -445,6 +445,8 @@ export type ProxyProviderTransportPermit = {
|
|
|
445
445
|
allowed: false;
|
|
446
446
|
errorCode: string | null;
|
|
447
447
|
transportScope: ProxyNetworkTransportScope;
|
|
448
|
+
/** The degrading failure happened before any request byte was sent. */
|
|
449
|
+
connectPhase: boolean;
|
|
448
450
|
};
|
|
449
451
|
export type ProxyAccountRoutingCandidate = {
|
|
450
452
|
account: string;
|
|
@@ -626,6 +628,8 @@ export type RequestAttemptLogEntry = {
|
|
|
626
628
|
transportScope?: ProxyTransportScope;
|
|
627
629
|
/** Whether this failed attempt may be retried without changing the request. */
|
|
628
630
|
retryable?: boolean;
|
|
631
|
+
/** The transport failure happened before any request byte was sent. */
|
|
632
|
+
connectPhase?: boolean;
|
|
629
633
|
/** Distinguishes short-lived admission throttles from exhausted quota windows. */
|
|
630
634
|
rateLimitKind?: "transient" | "quota";
|
|
631
635
|
/** Reset-aware cooldown reason selected for a rate-limited attempt. */
|
|
@@ -701,6 +705,8 @@ export type AnthropicAttemptLogger = (status: number, errorType?: string, errorM
|
|
|
701
705
|
cacheCreationTokens?: number;
|
|
702
706
|
cacheReadTokens?: number;
|
|
703
707
|
retryable?: boolean;
|
|
708
|
+
/** The transport failure happened before any request byte was sent. */
|
|
709
|
+
connectPhase?: boolean;
|
|
704
710
|
/** Low-level transport code such as ETIMEDOUT or EADDRNOTAVAIL. */
|
|
705
711
|
errorCode?: string;
|
|
706
712
|
transportScope?: ProxyTransportScope;
|
|
@@ -874,6 +880,9 @@ export type AnthropicUpstreamFetchResult = {
|
|
|
874
880
|
retrySameAccount?: boolean;
|
|
875
881
|
transportScope?: ProxyNetworkTransportScope;
|
|
876
882
|
errorCode?: string;
|
|
883
|
+
/** The transport failure happened while connecting, before any request
|
|
884
|
+
* byte was sent, so retrying it cannot duplicate provider work. */
|
|
885
|
+
connectPhase?: boolean;
|
|
877
886
|
/** When set, the caller should wait this many ms before retrying (from upstream retry-after). */
|
|
878
887
|
retryAfterMs?: number;
|
|
879
888
|
/** Set on a genuine 429: how long / why to cool this account before rotating. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.9.
|
|
3
|
+
"version": "12.9.5",
|
|
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": {
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
"test:local-usage": "pnpm exec tsx test/continuous-test-suite-local-usage.ts",
|
|
139
139
|
"test:dynamic": "pnpm exec tsx test/continuous-test-suite-dynamic.ts",
|
|
140
140
|
"test:proxy": "pnpm exec tsx test/continuous-test-suite-proxy.ts",
|
|
141
|
+
"test:proxy-connect-retry": "pnpm exec tsx test/continuous-test-suite-proxy-connect-retry.ts",
|
|
141
142
|
"test:codex": "pnpm exec tsx test/continuous-test-suite-codex.ts",
|
|
142
143
|
"test:bugfixes": "pnpm exec tsx test/continuous-test-suite-bugfixes.ts",
|
|
143
144
|
"test:json-e2e": "pnpm exec tsx test/continuous-test-suite-json-e2e.ts",
|