@juspay/neurolink 12.44.6 → 12.44.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 +2 -2
- package/dist/browser/neurolink.min.js +272 -272
- package/dist/proxy/accountCooldown.js +5 -2
- package/dist/proxy/accountQuota.js +2 -0
- package/dist/proxy/codexFallback.js +3 -4
- package/dist/server/routes/claudeProxyRoutes.js +7 -2
- package/dist/server/routes/codexProxyRoutes.js +24 -8
- package/docs-site/static/search-index.json +1 -1
- package/package.json +2 -1
|
@@ -98,7 +98,9 @@ export async function saveAccountCooldown(accountKey, coolingUntil, reason) {
|
|
|
98
98
|
await ensureAccountCooldownsLoaded();
|
|
99
99
|
// Another worker (a draining one during a rolling restart) may have written
|
|
100
100
|
// the file since this one loaded it, so the change folds onto the file as
|
|
101
|
-
// it is now rather than onto this worker's copy.
|
|
101
|
+
// it is now rather than onto this worker's copy. Best-effort: the mutex is
|
|
102
|
+
// per process, so two workers writing within the same few ms can still
|
|
103
|
+
// lose one update.
|
|
102
104
|
const onDisk = await readCooldownsFromDisk(memoryCache);
|
|
103
105
|
memoryCache = onDisk;
|
|
104
106
|
const current = onDisk[accountKey];
|
|
@@ -116,7 +118,8 @@ export async function clearAccountCooldown(accountKey, expectedCoolingUntil) {
|
|
|
116
118
|
await mutationMutex.runExclusive(async () => {
|
|
117
119
|
await ensureAccountCooldownsLoaded();
|
|
118
120
|
// Compare against the file, not this worker's copy: another worker may
|
|
119
|
-
// have extended this cooldown since, and that one must survive.
|
|
121
|
+
// have extended this cooldown since, and that one must survive. Same
|
|
122
|
+
// best-effort limit as saveAccountCooldown: no cross-process lock.
|
|
120
123
|
const onDisk = await readCooldownsFromDisk(memoryCache);
|
|
121
124
|
memoryCache = onDisk;
|
|
122
125
|
const current = onDisk[accountKey];
|
|
@@ -387,6 +387,8 @@ async function flushToDisk() {
|
|
|
387
387
|
// Another worker (a draining one during a rolling restart) may have
|
|
388
388
|
// written the file since this one loaded it: fold only this worker's
|
|
389
389
|
// changes onto the file as it is now, never rewrite it from this copy.
|
|
390
|
+
// Best-effort: the mutexes are per process, so two workers flushing
|
|
391
|
+
// within the same few ms can still lose one update.
|
|
390
392
|
const onDisk = await readQuotasFromDisk(filePath, fallback);
|
|
391
393
|
const merged = { ...onDisk };
|
|
392
394
|
for (const [key, quota] of Object.entries(flushing)) {
|
|
@@ -343,10 +343,9 @@ function codexSessionId(body) {
|
|
|
343
343
|
return undefined;
|
|
344
344
|
}
|
|
345
345
|
const fields = parsed;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
: undefined;
|
|
346
|
+
// `??` would let an empty or non-string session_id shadow a usable
|
|
347
|
+
// parent_session_id, so each field has to qualify on its own.
|
|
348
|
+
return [fields.session_id, fields.parent_session_id].find((value) => typeof value === "string" && value.length > 0);
|
|
350
349
|
}
|
|
351
350
|
/** Convert a Claude Messages request into the ChatGPT Codex Responses shape. */
|
|
352
351
|
export function convertClaudeRequestToCodex(body, model, reasoningEffort) {
|
|
@@ -773,8 +773,13 @@ function hasScopedOnlyExhaustion(quota, now, policy = overagePolicy) {
|
|
|
773
773
|
isOverageUsable(quota, policy)) {
|
|
774
774
|
return false;
|
|
775
775
|
}
|
|
776
|
-
|
|
777
|
-
|
|
776
|
+
// Same notion of "scoped" as the request path: header windows (which carry
|
|
777
|
+
// both a display scope and the wire id) and usage-API windows (often only
|
|
778
|
+
// one of them) must classify alike, or a 429 is scoped when routed and
|
|
779
|
+
// account-wide when reconciled.
|
|
780
|
+
return (quota.windows ?? []).some((window) => (window.kind === "weekly_scoped" || window.kind === "session_scoped") &&
|
|
781
|
+
(typeof window.scopeModel === "string" ||
|
|
782
|
+
typeof window.scopeModelId === "string") &&
|
|
778
783
|
now - scopedWindowObservedAt(quota, window) <=
|
|
779
784
|
QUOTA_SNAPSHOT_FRESHNESS_MS &&
|
|
780
785
|
isScopedWindowExhausted(window, now));
|
|
@@ -151,21 +151,25 @@ function buildCodexErrorResponse(status, message, code) {
|
|
|
151
151
|
}), { status, headers: { "content-type": "application/json" } });
|
|
152
152
|
}
|
|
153
153
|
/**
|
|
154
|
-
* Build a terminal Codex Responses SSE stream for a
|
|
155
|
-
*
|
|
154
|
+
* Build a terminal Codex Responses SSE stream for a failure that never reached
|
|
155
|
+
* an upstream request (every pooled account is cooling).
|
|
156
156
|
* A bare non-2xx status with no body reads to the Codex CLI as a dropped
|
|
157
157
|
* connection, so it shows "Reconnecting... waiting for network" forever
|
|
158
158
|
* instead of a real error. Emitting a well-formed `response.failed` event —
|
|
159
159
|
* the same terminal shape the CLI already parses out of a live stream —
|
|
160
160
|
* lets it render the actual failure instead.
|
|
161
|
+
*
|
|
162
|
+
* The code is what the client keys on: `insufficient_quota` tells it the plan
|
|
163
|
+
* is spent, so it is reserved for a pool that is out of quota. Accounts parked
|
|
164
|
+
* by a transient auth failure report `server_error` instead.
|
|
161
165
|
*/
|
|
162
|
-
function
|
|
166
|
+
function buildCodexPoolCoolingResponse(message, retryAfterSec, code) {
|
|
163
167
|
const payload = {
|
|
164
168
|
type: "response.failed",
|
|
165
169
|
response: {
|
|
166
170
|
error: {
|
|
167
|
-
type:
|
|
168
|
-
code
|
|
171
|
+
type: code,
|
|
172
|
+
code,
|
|
169
173
|
message,
|
|
170
174
|
},
|
|
171
175
|
},
|
|
@@ -1333,14 +1337,26 @@ loopbackPort, internalDispatch) {
|
|
|
1333
1337
|
});
|
|
1334
1338
|
return buildCodexErrorResponse(413, outbound.message, "request_too_large");
|
|
1335
1339
|
}
|
|
1340
|
+
const resetAt = soonest ? new Date(soonest).toISOString() : undefined;
|
|
1341
|
+
// A refresh that failed transiently, or a 401 with no refresh token to
|
|
1342
|
+
// retry with, parks the account as "auth". Reporting that as spent quota
|
|
1343
|
+
// would tell the user the plan is exhausted when it is not.
|
|
1344
|
+
if (ordered.every((account) => account.coolingReason === "auth")) {
|
|
1345
|
+
await recordFinalOutcome(undefined, 503, {
|
|
1346
|
+
errorType: "all_accounts_auth_cooling",
|
|
1347
|
+
errorMessage: "All Codex accounts are cooling after an auth failure",
|
|
1348
|
+
});
|
|
1349
|
+
return buildCodexPoolCoolingResponse(resetAt
|
|
1350
|
+
? `Codex authentication is temporarily unavailable for every account. Retry after ${resetAt}, or run \`neurolink auth login codex\` if it persists.`
|
|
1351
|
+
: "Codex authentication is temporarily unavailable for every account. Retry shortly, or run `neurolink auth login codex` if it persists.", retryAfterSec, "server_error");
|
|
1352
|
+
}
|
|
1336
1353
|
await recordFinalOutcome(undefined, 429, {
|
|
1337
1354
|
errorType: "all_accounts_cooling",
|
|
1338
1355
|
errorMessage: "All Codex accounts are rate-limited",
|
|
1339
1356
|
});
|
|
1340
|
-
|
|
1341
|
-
return buildCodexQuotaExhaustedResponse(resetAt
|
|
1357
|
+
return buildCodexPoolCoolingResponse(resetAt
|
|
1342
1358
|
? `Codex quota exhausted: all accounts are rate-limited. Resets at ${resetAt}.`
|
|
1343
|
-
: "Codex quota exhausted: all accounts are rate-limited.", retryAfterSec);
|
|
1359
|
+
: "Codex quota exhausted: all accounts are rate-limited.", retryAfterSec, "insufficient_quota");
|
|
1344
1360
|
}
|
|
1345
1361
|
let attempt = 0;
|
|
1346
1362
|
let lastErrorMessage = "All Codex accounts failed";
|
|
@@ -3201,7 +3201,7 @@
|
|
|
3201
3201
|
{"objectID":"8f00ff46e651a81fa28b61fd20f66b0a93b72d2024b4da2f2e8fe661b32c4d9e","title":"<<< neurolink-proxy (managed) <<<","url":"/docs/features/codex-proxy-support#-neurolink-proxy-managed--1","content":"Your original model_provider value is snapshotted to ~/.neurolink/codex-proxy-snapshot.json and restored on shutdown, so the edit is fully reversible even if the proxy crashes. The managed block is delimited by markers and removed cleanly on clear. If ~/.codex/config.toml` doesn't exist, the step is skipped silently.\n\nRestart Codex after starting the proxy for it to pick up the new provider.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"<<< neurolink-proxy (managed) <<<","lvl3":""}},
|
|
3202
3202
|
{"objectID":"404cd8f6d8542fc84651326cf0b252e7c3c3098678409fecaf8c2ff1559906fd","title":"4. Quota and routing","url":"/docs/features/codex-proxy-support#4-quota-and-routing","content":"Codex reports two rate-limit windows — primary (short) and secondary (weekly) — which map onto the shared AccountQuota model as the session and weekly fields respectively. That means Codex reuses the existing cooldown, persistence, and display code rather than duplicating it.\n\nAccount ordering is fill-first and quota-aware:\nAccounts on cooldown sort last.\nWithin the same cooldown state, accounts with a rejected unified quota sort after accounts without known rejection, even when their session usage is low or unknown.\nWithin each group, accounts with no session quota measurement sort first — they get probed so they become comparable, rather than being starved.\nOtherwise, least session utilization first. Rejected accounts remain eligible after preferred accounts so stale rejection evidence cannot permanently prevent a recovery probe.\n\nCooldown reasons map to the shared vocabulary: a rejected weekly window cools until its real reset (weekly), a rejected primary window until its reset (session), and a plain burst limit gets a bounded transient cooldown (60 s floor, 15 min ceiling).\n\nQuota and cooldown state are keyed by the full codex: account key, so a Codex account and an Anthropic account that share a bare label never collide.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"4. Quota and routing","lvl3":""}},
|
|
3203
3203
|
{"objectID":"acb592d28fcb90659fa175ac4f0d1e73cfb535d0aac2b63a101f0722e538eff7","title":"5. Response headers","url":"/docs/features/codex-proxy-support#5-response-headers","content":"A response served by the Codex pool carries these attribution headers. A\nresponse served by the outbound fallback\ncarries only x-neurolink-served-by: the account, attempt and quota headers\ndescribe a Codex account, and none served that turn.\n\n| Header | Meaning |\n| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| x-neurolink-account | Which pooled account served the request |\n| x-neurolink-account-type | Always codex-oauth |\n| x-neurolink-served-by | codex, or codex-outbound-fallback:anthropic / codex-outbound-fallback:vertex when the outbound fallback served the turn |\n| x-neurolink-attempt | Which attempt succeeded (1 = first account tried) |\n| x-neurolink-quota-source | live when the backend reported quota, else none |\n| x-neurolink-quota-session-left-pct | Remaining primary-window headroom |\n| x-neurolink-quota-weekly-left-pct | Canonical remaining","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"5. Response headers","lvl3":""}},
|
|
3204
|
-
{"objectID":"2b17d62c4d9cb65067100d6a90003bee2dce2bc06ede425f25f0b761aade359a","title":"6. Error handling","url":"/docs/features/codex-proxy-support#6-error-handling","content":"| Condition | Behaviour |\n| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| No Codex accounts configured | 401 with a message pointing at neurolink auth login codex |\n| All accounts cooling | Terminal response.failed SSE event (200, retry-after computed from the soonest recovery) instead of a bare 429, so the CLI shows a real error instead of \"Reconnecting...\" |\n| 401 / 403 from upstream | One forced token refresh, then rotate; a failed refresh disables the account until re-login |\n| 429 | Cool the account per its reported window, then rotate |\n| 5xx / network | Rotate to the next account |\n\nAccess tokens are refreshed proactively when within 5 minutes of expiry, and the rotated refresh token is written back to the store.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"6. Error handling","lvl3":""}},
|
|
3204
|
+
{"objectID":"2b17d62c4d9cb65067100d6a90003bee2dce2bc06ede425f25f0b761aade359a","title":"6. Error handling","url":"/docs/features/codex-proxy-support#6-error-handling","content":"| Condition | Behaviour |\n| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| No Codex accounts configured | 401 with a message pointing at neurolink auth login codex |\n| All accounts cooling | Terminal response.failed SSE event (200, retry-after computed from the soonest recovery) instead of a bare 429, so the CLI shows a real error instead of \"Reconnecting...\" |\n| All accounts auth-cooling | The same terminal event with code server_error instead of insufficient_quota (every account is parked by an auth failure), so it is not reported as spent quota |\n| 401 / 403 from upstream | One forced token refresh, then rotate; a failed refresh disables the account until re-login |\n| 429 | Cool the account per its reported window, then rotate |\n| 5xx / network | Rotate to the next account |\n\nAccess tokens are refreshed proactively when within 5 minutes of expiry, and the rotated refresh token is written back to the store.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"6. Error handling","lvl3":""}},
|
|
3205
3205
|
{"objectID":"9b1627d0fcff6a9e292b7f794fb47e62eb3da6b9c633e308da2b1de08a83368d","title":"7. Implementation map","url":"/docs/features/codex-proxy-support#7-implementation-map","content":"| File | Role |\n| ------------------------------------------- | ----------------------------------------------------------------------------------------- |\n| src/lib/types/codex.ts | Codex auth-file, token, and rate-limit types |\n| src/lib/auth/codexOAuth.ts | Endpoints/constants, auth.json import, token refresh, JWT decode, account-id resolution |\n| src/lib/proxy/codexAccountUsage.ts | Account enumeration, usage fetch, quota normalisation, header parsing |\n| src/lib/server/routes/codexProxyRoutes.ts | The pool engine: load → order → forward → rotate |\n| src/cli/commands/auth.ts | auth login codex, Codex rows in auth list --refresh |\n| src/cli/commands/proxy.ts | Route registration, /backend-api/* request tracking, ~/.codex/config.toml management |\n\nThe Anthropic engine in claudeProxyRoutes.ts is untouched. The Codex engine is deliberately leaner: it does pre-commit rotation but not the full transient-retry-budget or admission-lease machinery.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"7. Implementation map","lvl3":""}},
|
|
3206
3206
|
{"objectID":"c90a5222434bc717385c13b9e9f9cb865d3ce71ed7a58c077a7b5241def7d086","title":"8. Caveats","url":"/docs/features/codex-proxy-support#8-caveats","content":"Model ids matter. The ChatGPT backend rejects models that aren't available to Codex-with-a-ChatGPT-account (e.g. gpt-5-codex returns a 400). Use the model your Codex config already uses.\nTerms of service. Pooling multiple personal ChatGPT subscriptions through one client fingerprint is the kind of pattern subscription anti-abuse systems are built to detect. The originator, installation_id, user-agent, and turn-metadata headers are all correlatable. Pooling your own accounts is materially different from sharing across people — weigh the account-ban risk accordingly.\nNative browser login is not implemented. The verified path is importing an existing codex login credential. The OAuth constants (authorize URL, PKCE, scopes) are present in codexOAuth.ts for a future native flow.\nQuota-aware ordering is unverified against the live backend. The usage endpoint and the rate-limit header shape were reconstructed from a capture, not confirmed end to end. If either is wrong, fetchCodexAccountUsage returns no quota, x-neurolink-quota-source reads none, and ordering degenerates to insertion order while every 429 falls back to the 15-minute transient cooldown. Rotation still works; it is simply not quota-aware. Verify with neurolink auth list --refresh — a codex usage … error line per account means the quota path is not live.\nSSE usage-limit signals are not acted on. Only an HTTP 429 triggers a cooldown and rotation. A 200 response whose SSE stream carries usage_limit_reached (or the workspace-credit variants) is relayed to the client untouched, so the account is neither cooled nor rotated away from. HTTP-level exhaustion is handled; in-stream exhaustion is not.\nClient fingerprint is forwarded verbatim. The proxy replaces the caller's credentials but does not regenerate originator, installation_id, session-id, or turn metadata per account, so every pooled account shares the client's fingerprint. This is what makes the terms-of-service point above concrete.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"8. Caveats","lvl3":""}},
|
|
3207
3207
|
{"objectID":"607b92eb2c3b89a57438267a92b6212e616ad0dd106cb1c1a6bd0514c3446cd2","title":"Model discovery","url":"/docs/features/codex-proxy-support#model-discovery","content":"The Codex CLI refreshes its model list on every invocation:\n\nThe proxy relays that upstream to chatgpt.com/backend-api/codex/models using a\npooled account, forwarding the CLI's own query parameters.\n\nIt relays rather than synthesises, unlike the Claude and OpenAI /v1/models\nroutes, which build their lists locally from the model router. Which Codex\nmodels an account can reach is a property of that account — plan tier, rollout\nstate — not something this proxy knows, so a locally-built list would be a guess\nthat reads as authoritative.\n\nTwo details matter to anyone touching it:\nclient_version is required upstream. Omit it and ChatGPT answers 400\n with a pydantic Field required on ('query', 'client_version'). The query\n is rebuilt from ctx.query; ctx.path carries no query string, and reading it\n from there drops the parameter silently.\nDiscovery is side-effect free. No cooldown is recorded and no quota is\n consumed, so the once-per-invocation refresh cannot influence routing for real\n traffic. A cooling account is still allowed to answer it — being rate-limited\n for completions does not make an account unable to say which models exist.\n\nBefore this route existed the request 404'd, and the CLI printed\nfailed to refresh available models: unexpected status 404 Not Found on every\nrun before silently falling back to a default model — quietly ignoring the model\nthe user had configured.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"Model discovery","lvl3":""}},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.44.
|
|
3
|
+
"version": "12.44.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": {
|
|
@@ -188,6 +188,7 @@
|
|
|
188
188
|
"test:tool-routing-cli": "pnpm exec tsx test/continuous-test-suite-tool-routing-cli.ts",
|
|
189
189
|
"test:tool-dedup": "pnpm exec tsx test/continuous-test-suite-tool-dedup.ts",
|
|
190
190
|
"test:tools-manager-truncation": "pnpm exec tsx test/continuous-test-suite-tools-manager-truncation.ts",
|
|
191
|
+
"test:tooling-scripts": "pnpm exec tsx test/continuous-test-suite-tooling-scripts.ts",
|
|
191
192
|
"test:model-not-found-retryable": "pnpm exec tsx test/continuous-test-suite-model-not-found-retryable.ts",
|
|
192
193
|
"test:reasoning-parity-live": "pnpm exec tsx test/continuous-test-suite-reasoning-parity-live.ts",
|
|
193
194
|
"test:ci": "pnpm run test && pnpm run test:client && pnpm run test:hitl",
|