@juspay/neurolink 11.17.1 → 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.
@@ -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.1",
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": {