@juspay/neurolink 11.17.2 → 11.17.3
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 -0
- package/dist/auth/codexOAuth.js +1 -0
- package/dist/browser/neurolink.min.js +338 -338
- package/dist/server/routes/codexProxyRoutes.js +127 -1
- package/package.json +1 -1
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* the full transient-budget / admission machinery.
|
|
19
19
|
*/
|
|
20
20
|
import { tokenStore } from "../../auth/tokenStore.js";
|
|
21
|
-
import { CODEX_ORIGINATOR, CODEX_RESPONSES_URL, CODEX_USER_AGENT, codexTokenNeedsRefresh, isPermanentCodexRefreshFailure, refreshCodexToken, resolveCodexAccountId, } from "../../auth/codexOAuth.js";
|
|
21
|
+
import { CODEX_ORIGINATOR, CODEX_MODELS_URL, CODEX_RESPONSES_URL, CODEX_USER_AGENT, codexTokenNeedsRefresh, isPermanentCodexRefreshFailure, refreshCodexToken, resolveCodexAccountId, } from "../../auth/codexOAuth.js";
|
|
22
22
|
import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from "../../proxy/accountCooldown.js";
|
|
23
23
|
import { loadAccountQuotas, saveAccountQuota, } from "../../proxy/accountQuota.js";
|
|
24
24
|
import { createCodexUsageTap } from "../../proxy/codexUsage.js";
|
|
@@ -452,6 +452,126 @@ async function handleCodexResponsesRequest(ctx) {
|
|
|
452
452
|
});
|
|
453
453
|
return buildCodexErrorResponse(lastErrorStatus, lastErrorMessage);
|
|
454
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Relay Codex model discovery upstream.
|
|
457
|
+
*
|
|
458
|
+
* The CLI refreshes its model list on every invocation. Only `/responses` was
|
|
459
|
+
* registered, so that GET 404'd and the CLI printed a refresh failure before
|
|
460
|
+
* falling back to a default model — quietly ignoring the model the user had
|
|
461
|
+
* configured.
|
|
462
|
+
*
|
|
463
|
+
* This relays rather than synthesises, unlike the Claude and OpenAI `/v1/models`
|
|
464
|
+
* routes, which build their lists locally from the model router. Codex model
|
|
465
|
+
* availability is a property of the upstream account (plan tier, rollout), not
|
|
466
|
+
* of anything this proxy knows, so a synthesised list would be a guess that
|
|
467
|
+
* looks authoritative.
|
|
468
|
+
*
|
|
469
|
+
* Read-only with respect to ROUTING: no cooldown is recorded and no quota is
|
|
470
|
+
* consumed, so a discovery call cannot influence which account real traffic
|
|
471
|
+
* lands on. It is not literally side-effect free — a token refreshed below is
|
|
472
|
+
* persisted, exactly as the proactive refresh in `loadCodexProxyAccounts`
|
|
473
|
+
* persists one. What discovery deliberately never does is *penalise* an
|
|
474
|
+
* account: it cannot cool one and cannot disable one. A read-only probe must
|
|
475
|
+
* not be able to cost the user a login.
|
|
476
|
+
*/
|
|
477
|
+
async function handleCodexModelsRequest(ctx) {
|
|
478
|
+
const accounts = await loadCodexProxyAccounts();
|
|
479
|
+
if (accounts.length === 0) {
|
|
480
|
+
return buildCodexErrorResponse(401, "No Codex accounts configured. Run `neurolink auth login codex`.");
|
|
481
|
+
}
|
|
482
|
+
const now = Date.now();
|
|
483
|
+
const ordered = orderCodexAccounts(accounts, now);
|
|
484
|
+
// A cooling account is rate-limited for completions, not barred from
|
|
485
|
+
// answering what models exist. Healthy accounts go first, but a cooling one
|
|
486
|
+
// is still a candidate rather than a reason to fail discovery outright.
|
|
487
|
+
const isCooling = (a) => a.coolingUntil !== undefined && a.coolingUntil > now;
|
|
488
|
+
const candidates = [
|
|
489
|
+
...ordered.filter((a) => !isCooling(a)),
|
|
490
|
+
...ordered.filter(isCooling),
|
|
491
|
+
];
|
|
492
|
+
// Forward the CLI's own query — it sends client_version, and upstream
|
|
493
|
+
// *requires* it: without it ChatGPT answers 400 with a pydantic
|
|
494
|
+
// "Field required" on ('query','client_version'). Rebuild from ctx.query,
|
|
495
|
+
// not ctx.path: path carries no query string, so reading it there silently
|
|
496
|
+
// dropped the parameter and produced exactly that 400.
|
|
497
|
+
const params = new URLSearchParams(ctx.query ?? {});
|
|
498
|
+
const query = params.toString();
|
|
499
|
+
const url = query ? `${CODEX_MODELS_URL}?${query}` : CODEX_MODELS_URL;
|
|
500
|
+
let lastErrorStatus = 502;
|
|
501
|
+
let lastErrorMessage = "Codex model discovery upstream failed";
|
|
502
|
+
for (const account of candidates) {
|
|
503
|
+
// One forced refresh per account, then move on. A token can be rejected
|
|
504
|
+
// upstream while still inside its local expiry window, so relaying that
|
|
505
|
+
// 401 straight back left discovery broken until the token expired locally
|
|
506
|
+
// — the CLI would fall back to a default model on every invocation in the
|
|
507
|
+
// meantime.
|
|
508
|
+
let authRetried = false;
|
|
509
|
+
for (;;) {
|
|
510
|
+
let upstream;
|
|
511
|
+
try {
|
|
512
|
+
upstream = await fetch(url, {
|
|
513
|
+
method: "GET",
|
|
514
|
+
headers: buildCodexUpstreamHeaders(ctx.headers ?? {}, account),
|
|
515
|
+
// Bound the upstream call, as the responses route does. Without a
|
|
516
|
+
// signal a stalled connection holds the proxy request open with no
|
|
517
|
+
// ceiling, and the CLI blocks on model discovery at startup.
|
|
518
|
+
signal: AbortSignal.timeout(CODEX_UPSTREAM_TIMEOUT_MS),
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
lastErrorStatus = 502;
|
|
523
|
+
lastErrorMessage = `Codex model discovery upstream failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
524
|
+
break; // rotate to the next account
|
|
525
|
+
}
|
|
526
|
+
if (upstream.status === 401 || upstream.status === 403) {
|
|
527
|
+
if (!authRetried && account.refreshToken) {
|
|
528
|
+
authRetried = true;
|
|
529
|
+
try {
|
|
530
|
+
const refreshed = await refreshCodexToken(account.refreshToken);
|
|
531
|
+
account.token = refreshed.accessToken;
|
|
532
|
+
account.refreshToken =
|
|
533
|
+
refreshed.refreshToken ?? account.refreshToken;
|
|
534
|
+
account.expiresAt = refreshed.expiresAt ?? account.expiresAt;
|
|
535
|
+
account.accountId = resolveCodexAccountId(refreshed.accessToken);
|
|
536
|
+
await tokenStore.saveTokens(account.key, {
|
|
537
|
+
accessToken: account.token,
|
|
538
|
+
refreshToken: account.refreshToken,
|
|
539
|
+
expiresAt: account.expiresAt ?? Date.now() + 3_600_000,
|
|
540
|
+
tokenType: "Bearer",
|
|
541
|
+
});
|
|
542
|
+
continue; // retry this account with the fresh token
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
// No cooldown and no disable, unlike the responses path: the
|
|
546
|
+
// verdict a completion draws from a failed refresh is earned by a
|
|
547
|
+
// request the user actually made. Discovery fires on every CLI
|
|
548
|
+
// invocation, so letting it disable an account would turn a
|
|
549
|
+
// background probe into a forced re-login.
|
|
550
|
+
lastErrorStatus = 401;
|
|
551
|
+
lastErrorMessage = "Codex token refresh failed; re-login required";
|
|
552
|
+
break; // rotate
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
lastErrorStatus = upstream.status;
|
|
556
|
+
lastErrorMessage = "Codex model discovery rejected upstream";
|
|
557
|
+
break; // rotate
|
|
558
|
+
}
|
|
559
|
+
// Every other status — including a 400 — is upstream's real answer to a
|
|
560
|
+
// well-formed request and is relayed unchanged. A 400 here means the
|
|
561
|
+
// query was not forwarded correctly, and hiding it behind a retry would
|
|
562
|
+
// bury the exact regression this route was added to fix.
|
|
563
|
+
const body = await upstream.text();
|
|
564
|
+
const contentType = upstream.headers.get("content-type") ?? "application/json";
|
|
565
|
+
return new Response(body, {
|
|
566
|
+
status: upstream.status,
|
|
567
|
+
headers: { "content-type": contentType },
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// A discovery failure must not look like a missing route, or the next
|
|
572
|
+
// person debugging it re-opens this same issue.
|
|
573
|
+
return buildCodexErrorResponse(lastErrorStatus, lastErrorMessage);
|
|
574
|
+
}
|
|
455
575
|
/**
|
|
456
576
|
* Create Codex proxy routes.
|
|
457
577
|
*
|
|
@@ -468,6 +588,12 @@ export function createCodexProxyRoutes(basePath = "") {
|
|
|
468
588
|
description: "Codex ChatGPT-backend Responses API (account pool)",
|
|
469
589
|
handler: (ctx) => handleCodexResponsesRequest(ctx),
|
|
470
590
|
},
|
|
591
|
+
{
|
|
592
|
+
method: "GET",
|
|
593
|
+
path: `${basePath}/backend-api/codex/models`,
|
|
594
|
+
description: "Codex model discovery, relayed upstream (account pool)",
|
|
595
|
+
handler: (ctx) => handleCodexModelsRequest(ctx),
|
|
596
|
+
},
|
|
471
597
|
],
|
|
472
598
|
};
|
|
473
599
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "11.17.
|
|
3
|
+
"version": "11.17.3",
|
|
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": {
|