@juspay/neurolink 13.8.13 → 13.8.14
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.
|
@@ -24,7 +24,7 @@ import { loadAccountQuotas, saveAccountQuota, } from "../../proxy/accountQuota.j
|
|
|
24
24
|
import { createCodexUsageTap } from "../../proxy/codexUsage.js";
|
|
25
25
|
import { CODEX_ACCOUNT_PREFIX, parseCodexRateLimitHeaders, } from "../../proxy/codexAccountUsage.js";
|
|
26
26
|
import { buildClientAttribution } from "../../proxy/clientAttribution.js";
|
|
27
|
-
import { getProxyUpstreamFailure } from "../../proxy/proxyFailureDetails.js";
|
|
27
|
+
import { classifyProxyFailureCode, getProxyUpstreamFailure, } from "../../proxy/proxyFailureDetails.js";
|
|
28
28
|
import { registerProxyResponseObserver, isProxyRequestFinalized, registerInternalProxyRequest, getProxyBridgeResult, releaseProxyRequestAccounting, } from "../../proxy/proxyActivity.js";
|
|
29
29
|
import { parseCodexNativeRequest, translateCodexRequestToClaude, classifyCodexOutboundFailure, buildSystemBlocksFromDeveloperMessages, } from "../../proxy/codexOutboundFallback.js";
|
|
30
30
|
import { createAnthropicFallbackStream } from "../../proxy/codexToAnthropicFallback.js";
|
|
@@ -77,6 +77,32 @@ function codexTransportScope(error) {
|
|
|
77
77
|
function summarizeCodexUpstreamError(errorText, fallback) {
|
|
78
78
|
return sanitizeForLog(errorText).slice(0, 200) || fallback;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether a pre-stream 401/403 refuses the REQUEST rather than the credential.
|
|
82
|
+
*
|
|
83
|
+
* Only the body can tell: a content-policy 403 and an expired-token 403 share a
|
|
84
|
+
* status. `classifyProxyFailureCode` is the verdict the outbound fallback stops
|
|
85
|
+
* on, so the pool and the fallback read one code the same way. Anything it does
|
|
86
|
+
* not call non-retryable, including a body with no code at all, stays an
|
|
87
|
+
* authentication failure.
|
|
88
|
+
*/
|
|
89
|
+
function readCodexRequestRefusal(status, errorText) {
|
|
90
|
+
if (status !== 401 && status !== 403) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const failure = getProxyUpstreamFailure({ responseBody: errorText });
|
|
94
|
+
if (failure?.code === undefined) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const known = classifyProxyFailureCode(failure.code);
|
|
98
|
+
return known.retryable === false
|
|
99
|
+
? {
|
|
100
|
+
code: failure.code,
|
|
101
|
+
status: known.status ?? status,
|
|
102
|
+
message: failure.message,
|
|
103
|
+
}
|
|
104
|
+
: undefined;
|
|
105
|
+
}
|
|
80
106
|
/**
|
|
81
107
|
* In-flight proactive refreshes, keyed by account.
|
|
82
108
|
*
|
|
@@ -1389,6 +1415,32 @@ async function handleCodexUpstreamFailure(rt, args) {
|
|
|
1389
1415
|
const { writeAttempt } = rt;
|
|
1390
1416
|
const { account, attempt, attemptStartedAt, upstream, authRetried } = args;
|
|
1391
1417
|
const errText = await upstream.text().catch(() => "");
|
|
1418
|
+
// A refusal of the request itself says nothing about the account: a refresh
|
|
1419
|
+
// would rotate its refresh token for nothing, a cooldown would park a healthy
|
|
1420
|
+
// account, and every other account would be refused the same way.
|
|
1421
|
+
const refusal = readCodexRequestRefusal(upstream.status, errText);
|
|
1422
|
+
if (refusal) {
|
|
1423
|
+
const errorMessage = refusal.message ??
|
|
1424
|
+
summarizeCodexUpstreamError(errText, "Codex refused the request");
|
|
1425
|
+
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, upstream.status);
|
|
1426
|
+
writeAttempt(account, attempt, attemptStartedAt, upstream.status, {
|
|
1427
|
+
errorType: "invalid_request_error",
|
|
1428
|
+
errorCode: refusal.code,
|
|
1429
|
+
errorMessage,
|
|
1430
|
+
retryable: false,
|
|
1431
|
+
});
|
|
1432
|
+
return {
|
|
1433
|
+
lastFailure: {
|
|
1434
|
+
errorType: "invalid_request_error",
|
|
1435
|
+
errorCode: refusal.code,
|
|
1436
|
+
retryable: false,
|
|
1437
|
+
},
|
|
1438
|
+
lastErrorStatus: refusal.status,
|
|
1439
|
+
lastErrorCode: refusal.code,
|
|
1440
|
+
lastErrorMessage: errorMessage,
|
|
1441
|
+
refusedRequest: true,
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1392
1444
|
// 401/403 → try a forced token refresh once, then rotate.
|
|
1393
1445
|
if ((upstream.status === 401 || upstream.status === 403) &&
|
|
1394
1446
|
!authRetried &&
|
|
@@ -1486,9 +1538,10 @@ async function handleCodexUpstreamFailure(rt, args) {
|
|
|
1486
1538
|
}
|
|
1487
1539
|
// Other non-ok → record and rotate.
|
|
1488
1540
|
if (upstream.status === 401 || upstream.status === 403) {
|
|
1489
|
-
// Reached
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1541
|
+
// Reached when the account has no refresh token to retry with, or when
|
|
1542
|
+
// the retry after a refresh was rejected again. Either way it will fail
|
|
1543
|
+
// identically on the next request, so park it briefly instead of letting
|
|
1544
|
+
// it stay first in line with unknown quota.
|
|
1492
1545
|
await saveAccountCooldown(account.key, Date.now() + CODEX_AUTH_COOLDOWN_MS, "auth").catch(() => undefined);
|
|
1493
1546
|
}
|
|
1494
1547
|
const errorMessage = summarizeCodexUpstreamError(errText, "Codex error");
|
|
@@ -1733,7 +1786,8 @@ loopbackPort, internalDispatch) {
|
|
|
1733
1786
|
let lastErrorCode;
|
|
1734
1787
|
let lastFailure = { errorType: "all_accounts_failed" };
|
|
1735
1788
|
let lastAttemptedAccount;
|
|
1736
|
-
|
|
1789
|
+
let requestRefused = false;
|
|
1790
|
+
accountLoop: for (const account of eligible) {
|
|
1737
1791
|
try {
|
|
1738
1792
|
tracer?.setAccountSelection({
|
|
1739
1793
|
strategy: "codex-quota-order",
|
|
@@ -1859,6 +1913,11 @@ loopbackPort, internalDispatch) {
|
|
|
1859
1913
|
}
|
|
1860
1914
|
({ lastFailure, lastErrorStatus, lastErrorCode, lastErrorMessage } =
|
|
1861
1915
|
failure);
|
|
1916
|
+
if (failure.refusedRequest) {
|
|
1917
|
+
// Every other account would be refused the same way.
|
|
1918
|
+
requestRefused = true;
|
|
1919
|
+
break accountLoop;
|
|
1920
|
+
}
|
|
1862
1921
|
break;
|
|
1863
1922
|
}
|
|
1864
1923
|
}
|
|
@@ -1874,7 +1933,7 @@ loopbackPort, internalDispatch) {
|
|
|
1874
1933
|
...lastFailure,
|
|
1875
1934
|
errorMessage: lastFailure.errorMessage ?? lastErrorMessage,
|
|
1876
1935
|
});
|
|
1877
|
-
return buildCodexErrorResponse(lastErrorStatus, lastErrorMessage);
|
|
1936
|
+
return buildCodexErrorResponse(lastErrorStatus, lastErrorMessage, requestRefused ? lastErrorCode : undefined);
|
|
1878
1937
|
};
|
|
1879
1938
|
try {
|
|
1880
1939
|
return await dispatch();
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -1068,6 +1068,10 @@ export type CodexRotationFailure = {
|
|
|
1068
1068
|
lastErrorStatus: number;
|
|
1069
1069
|
lastErrorCode: string | undefined;
|
|
1070
1070
|
lastErrorMessage: string;
|
|
1071
|
+
/** The upstream refused the request itself (a policy or invalid-request
|
|
1072
|
+
* code), not the account: no other account would answer differently, so the
|
|
1073
|
+
* pool ends the request here instead of rotating. */
|
|
1074
|
+
refusedRequest?: boolean;
|
|
1071
1075
|
};
|
|
1072
1076
|
/** Minimal persistence contract needed by Codex rotating-token refreshes. */
|
|
1073
1077
|
export type CodexRefreshTokenStore = {
|
|
@@ -2672,13 +2672,13 @@
|
|
|
2672
2672
|
{"objectID":"2c1717347ebf0ec18b9aa2738c5628682d4318c893427fdc5267caed3fe54845","title":"3. Client auto-configuration","url":"/docs/features/codex-proxy-support#3-client-auto-configuration","content":"When neurolink proxy start runs (non-dev), it configures the Codex CLI the same way it configures Claude Code and OpenCode. It appends a marker-delimited block to ~/.codex/config.toml:\n\nYour 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":"3. Client auto-configuration","lvl3":""}},
|
|
2673
2673
|
{"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":""}},
|
|
2674
2674
|
{"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":""}},
|
|
2675
|
-
{"objectID":"2b17d62c4d9cb65067100d6a90003bee2dce2bc06ede425f25f0b761aade359a","title":"6. Error handling","url":"/docs/features/codex-proxy-support#6-error-handling","content":"| Condition
|
|
2675
|
+
{"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 and a retry; a second rejection parks the account for 60 s and rotates; a failed refresh disables the account until re-login |\n| 401 / 403 with a request-refusal code | The request ends on that account with no refresh, cooldown or rotation: a policy code answers 403, an invalid-request code keeps the upstream status (see below) |\n| 429 | Cool the account per its reported window, then rotate |\n| 5xx / network | Rotate to the next account |","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"6. Error handling","lvl3":""}},
|
|
2676
2676
|
{"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":""}},
|
|
2677
2677
|
{"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":""}},
|
|
2678
2678
|
{"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":""}},
|
|
2679
2679
|
{"objectID":"6d2395f1bb843928fffc947203d5d331fb549bb7d9403adfa777e676578b4d18","title":"Status: implemented, default OFF, request-shape verified only — see \"Live validation\" below","url":"/docs/features/codex-proxy-support#status-implemented-default-off-request-shape-verified-only-see-live-validation-below","content":"Sections 1-8 above cover the inbound direction: a Codex CLI request served\nby the Codex/ChatGPT pool engine. This section covers the opposite direction —\na native Codex /backend-api/codex/responses request that, when every Codex\naccount is exhausted or otherwise ineligible, is translated into an\nAnthropic Messages request and served by either the Anthropic OAuth pool (via\nan in-process loopback, no extra network hop to this same proxy) or a\nVertex-Claude passthrough account, then translated back into the Codex\nResponses wire shape the CLI expects. It is a third direction alongside the\ntwo-engine matrix in the rest of this document, not a replacement for either.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"Status: implemented, default OFF, request-shape verified only — see \"Live validation\" below","lvl3":""}},
|
|
2680
2680
|
{"objectID":"3c2f324de0830846bbdce959aabe14903475260a2adfabf478b669fef52ac726","title":"9.1 Enabling it","url":"/docs/features/codex-proxy-support#91-enabling-it","content":"The kill switch defaults to off. Nothing changes for an existing Codex\npool setup until you opt in:\n\nor via environment variable, which overrides the YAML key:\n\ncodex-outbound-fallback-targets is required (non-empty) for the fallback to\never fire — the kill switch alone is not enough. provider must be anthropic\nor vertex; up to 8 targets are accepted and validated at config-load time.\nWith the flag off, or with no targets configured, an exhausted Codex request\nbehaves exactly as it did before this feature existed: the same terminal\nresponse.failed SSE / 401 behaviour documented in section 6 above, byte-\nfor-byte.\n\nThe anthropic target needs the CLI proxy's in-process loopback. Dispatch\nto an anthropic target goes through an in-process loopback\n(127.0.0.1:<loopbackPort> plus an unforgeable internalDispatch marker, see\n§9.3) rather than a second network hop, and only neurolink proxy start (the\nCLI proxy command) wires that loopback through today. createAllRoutes /\nregisterAllRoutes — the generic SDK route surface in\nsrc/lib/server/routes/index.ts, used when embedding the proxy routes in your\nown server — does not thread loopbackPort/internalDispatch into\ncreateCodexProxyRoutes, so enabling this feature there with an anthropic\ntarget configured cannot actually dispatch: route creation logs one warning\nand every such request falls through to the normal Codex-exhaustion behaviour\nin section 6, indistinguishable on the wire from the feature being off. A\nvertex target has no such gap — it dispatches over executeVertexAnthropicFallback\nregardless of which route surface created the routes. If you need the\nanthropic target from the SDK route surface, run the CLI proxy alongside\nyour server, or wait for loopbackPort/internalDispatch to be threaded\nthrough CreateRoutesOptions (not yet implemented — see the doc comments on\ncreateAllRoutes and CreateRoutesOptions.runtimeConfigProvider).","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"9.1 Enabling it","lvl3":""}},
|
|
2681
|
-
{"objectID":"d20a853143464660ecb4669832c8c0d5e24292564bc7ea9161354afda9a39223","title":"9.2 When it triggers","url":"/docs/features/codex-proxy-support#92-when-it-triggers","content":"attemptCodexOutboundFallback (module-private in codexProxyRoutes.ts) is\nconsulted only after every Codex account has been tried and failed, and only\nwhen all of the following hold, in this order — any one failing gate means\n\"not attempted\", and the request falls through to the normal Codex-exhaustion\nbehaviour in section 6:\nThe inbound request is not itself already a fallback leg (see loop\n prevention below).\nruntimeConfigProvider is configured (the proxy is running with the\n runtime-config feature enabled at all).\nclassifyCodexOutboundFailure judges the failure itself eligible — a total\n function, not a heuristic guess, over the terminal Codex failure class.\nThe kill switch (codexOutboundFallbackEnabled) is on.\ncodexOutboundFallbackTargets is non-empty.\nThe native Codex request body parses (parseCodexNativeRequest) — a\n request the translator cannot even parse is never a candidate.\n\nWhen the account loop runs out, the last attempt decides eligibility. A\n401/403 across the whole pool is a Codex credential failure and falls back, as\ndo 429 and 5xx. A content-policy or invalid-request error code\n(classifyProxyFailureCode), or any other 4xx, rejects the request itself and\nis returned unchanged.\n\nThe Anthropic loopback has a 5-minute timeout that covers the connection until\nits response headers arrive. After that the stream runs as long as the turn\ndoes; a client disconnect still ends it.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"9.2 When it triggers","lvl3":""}},
|
|
2681
|
+
{"objectID":"d20a853143464660ecb4669832c8c0d5e24292564bc7ea9161354afda9a39223","title":"9.2 When it triggers","url":"/docs/features/codex-proxy-support#92-when-it-triggers","content":"attemptCodexOutboundFallback (module-private in codexProxyRoutes.ts) is\nconsulted only after every Codex account has been tried and failed, and only\nwhen all of the following hold, in this order — any one failing gate means\n\"not attempted\", and the request falls through to the normal Codex-exhaustion\nbehaviour in section 6:\nThe inbound request is not itself already a fallback leg (see loop\n prevention below).\nruntimeConfigProvider is configured (the proxy is running with the\n runtime-config feature enabled at all).\nclassifyCodexOutboundFailure judges the failure itself eligible — a total\n function, not a heuristic guess, over the terminal Codex failure class.\nThe kill switch (codexOutboundFallbackEnabled) is on.\ncodexOutboundFallbackTargets is non-empty.\nThe native Codex request body parses (parseCodexNativeRequest) — a\n request the translator cannot even parse is never a candidate.\n\nWhen the account loop runs out, the last attempt decides eligibility. A\n401/403 across the whole pool is a Codex credential failure and falls back, as\ndo 429 and 5xx. A content-policy or invalid-request error code\n(classifyProxyFailureCode), or any other 4xx, rejects the request itself and\nis returned unchanged. A pre-stream 401/403 carrying such a code never reaches\nthis point: the pool ends the request on the account that received it (see\nsection 6).\n\nThe Anthropic loopback has a 5-minute timeout that covers the connection until\nits response headers arrive. After that the stream runs as long as the turn\ndoes; a client disconnect still ends it.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"9.2 When it triggers","lvl3":""}},
|
|
2682
2682
|
{"objectID":"63d9622db4658e47ba8122e511162860b322d42d2b741a5c76611250fe670550","title":"9.3 Loop prevention","url":"/docs/features/codex-proxy-support#93-loop-prevention","content":"Two independent guards, so a defect in one does not remove the bound:\nMarker guard. The moment every gate above has passed and a real\n dispatch attempt is about to begin, codexProxyRoutes.ts sets\n ctx.metadata[\"neurolink.codexOutboundFallbackAttempted\"] = true on the\n request context. Gate 1 above checks this same key, so a request that has\n already crossed engines once can never be offered a second outbound-fallback\n attempt within the same client request. This is a single flag, not a\n per-target counter, because at most one crossing is ever allowed regardless\n of how many of the internal dispatch call sites reach this function.\nDepth counter (defense-in-depth). ctx.metadata[\"neurolink.fallbackHopDepth\"]\n is incremented on every engine crossing (never on a same-engine account\n rotation) and checked against MAX_ENGINE_CROSSINGS = 1, independent of the\n marker check above. The invariant this enforces: the ordered sequence of\n engines serving any one client request has length ≤ 2 with no repeat — only\n [codex], [anthropic], [codex,anthropic], [anthropic,codex] are\n possible. A leg that is itself already an inner fallback of the other engine\n can never itself fall outbound again.\n\nA request that reaches this point already carrying a forged\ncodexOutboundFallbackAttempted-shaped marker from outside is not trusted —\nthe key lives on the server-side request context (ctx.metadata), not on\nanything the client can set on the wire, so there is no client-controlled way\nto spoof past gate 1.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"9.3 Loop prevention","lvl3":""}},
|
|
2683
2683
|
{"objectID":"21189de88d4ffef1c2a4bbd0420442de71fe8d9fd6b9f6278867e39602e075c6","title":"9.4 What gets translated","url":"/docs/features/codex-proxy-support#94-what-gets-translated","content":"Request translation (src/lib/proxy/codexOutboundFallback.ts):\nCodex additional_tools namespaces (functions, collaboration, …) are\n flattened into one Claude tools array, preserving declaration order —\n namespace order, then tool order within each namespace — never regrouped or\n resorted by tool kind.\nA tool's kind (function vs custom) is read from the tool declaration's\n own type field, never inferred from which namespace it was declared under.\nFunction-tool JSON Schemas are dereferenced with inlineJsonSchema: plain,\n resolvable $ref/$defs (draft-07 definitions and 2020-12 $defs) are\n flattened into the inline schema and the pointer keys stripped entirely.\n A genuinely circular $ref degrades to {type: \"object\"} and is counted\n under the circular_ref_flattened degrade reason (see metrics below); a\n plain non-circular $ref is not.\nGrammar/custom tools (Lark grammar, regex format) have no JSON Schema\n equivalent, so they are wrapped as a single {input: string} parameter, with\n the response mapped back to a custom_tool_call item. This is lossy and\n explicitly flagged in code, and is the shipped behavior — see \"Sign-off\n status\" below.\nReasoning effort (minimal/low/medium/high/xhigh/max on the Codex\n side) maps to an Anthropic thinking budget via mapCodexReasoningToThinking:\n low→4096, medium→8192, high→16384, xhigh→24576, max→32768, clamped to\n [1024, max_tokens - 1024]. Thinking is left off when tool_choice is\n forced, or when the immediately preceding assistant turn called a tool.\nA non-object function_call_output/custom_tool_call_output argument falls\n back to {input: raw} in the translated history rather than failing the\n translation.\n\nResponse translation (codexResponsesFormat.ts / codexToAnthropicFallback.ts)\nconverts the Claude response — including SSE streaming — back into the Codex\nResponses wire shape the CLI expects, so a client cannot tell, from the wire\nalone, that its request was served by a different engine underneath.","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"9.4 What gets translated","lvl3":""}},
|
|
2684
2684
|
{"objectID":"4a8c83d10dbaa054203c48c374f7aed2688cb58670135943b0b2c6cd358cf129","title":"9.5 Accounting","url":"/docs/features/codex-proxy-support#95-accounting","content":"A translated request is billed and recorded exactly like any other proxied\nrequest on the serving engine — the same OTel usage record, cache/reasoning\nbuckets, and cost estimate described in Usage accounting\n(section 1) apply, attributed to the Anthropic or Vertex account that actually\nserved it, not to a Codex account. There is no separate \"fallback\" ledger; look\nat the served-by account's own usage.\n\nThe Codex route's final record names what served the turn: provider,\nmodel, account and accountType are the Claude target's, and\nrequestedModel stays the Codex model the client asked for. With the Anthropic\ntarget, the loopback's own /v1/messages record owns the usage, and the Codex\nrecord points to it through usageOwnerRequestId, so the call is counted once.\nWith Vertex there is no child record, so the Codex record carries the usage and\nis priced from the Vertex table.\n\nThe translated request marks its stable prefix with a 1-hour cache TTL. 1-hour\nwrites (usage.cache_creation.ephemeral_1h_input_tokens) are recorded as\ncacheCreation1hTokens, a subset of the cache-write total, and priced at 2x\nbase input, Anthropic's 1-hour rate on every Claude model. The rest of the\ncache writes keep the 5-minute rate (1.25x).\n\nThe token/cost metrics carry an origin attribute (ProxyRequestOrigin:\n\"native\" | \"codex-fallback\"), but only when it is not \"native\" — a\nplain Codex or Claude request emits the exact same label set it always has,\nwith no origin key at all, so existing dashboards and alerts over these\nmetrics are unaffected. Only a request actually served by this outbound-fallback\npath carries origin=\"codex-fallback\", set via\ntracer.setRequestOrigin(\"codex-fallback\") once a target has been dispatched\nand will serve the turn (both the Anthropic-loopback and Vertex targets); a\nrequest whose every target fails keeps \"native\".","hierarchy":{"lvl0":"Features","lvl1":"Codex (ChatGPT) Support for NeuroLink Proxy","lvl2":"9.5 Accounting","lvl3":""}},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "13.8.
|
|
4
|
-
"packageManager": "pnpm@10.
|
|
3
|
+
"version": "13.8.14",
|
|
4
|
+
"packageManager": "pnpm@10.28.2",
|
|
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": {
|
|
7
7
|
"name": "Juspay Technologies",
|
|
@@ -242,6 +242,7 @@
|
|
|
242
242
|
"test:proxy-telemetry": "pnpm exec tsx test/continuous-test-suite-proxy-telemetry.ts",
|
|
243
243
|
"test:proxy-restart": "pnpm exec tsx test/continuous-test-suite-proxy-restart.ts",
|
|
244
244
|
"test:proxy-image-input": "pnpm exec tsx test/continuous-test-suite-proxy-image-input.ts",
|
|
245
|
+
"test:proxy-codex-policy-refusal": "pnpm exec tsx test/continuous-test-suite-proxy-codex-policy-refusal.ts",
|
|
245
246
|
"test:codex": "pnpm exec tsx test/continuous-test-suite-codex.ts",
|
|
246
247
|
"test:bugfixes": "pnpm exec tsx test/continuous-test-suite-bugfixes.ts",
|
|
247
248
|
"test:json-e2e": "pnpm exec tsx test/continuous-test-suite-json-e2e.ts",
|