@juspay/neurolink
Version:
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
2,140 lines • 104 kB
JavaScript
/**
* Codex (OpenAI ChatGPT) Proxy Routes
*
* Exposes `POST /backend-api/codex/responses` — the ChatGPT-backend Responses
* API endpoint the Codex CLI talks to — and pools it across multiple ChatGPT
* OAuth accounts, mirroring the Anthropic Claude pool engine.
*
* A Codex CLI configured with `model_providers.<name>.base_url` pointing at this
* proxy sends its own OAuth token; the proxy strips it, selects a pooled
* `codex:*` account, attaches that account's Bearer + matching chatgpt-account-id,
* forwards to https://chatgpt.com/backend-api/codex, and relays the SSE stream.
* On quota exhaustion (429 / usage limit) the account is cooled and the next
* account is tried — so the user never has to switch accounts by hand.
*
* This engine is deliberately leaner than claudeProxyRoutes.ts: it reuses the
* shared cooldown/quota persistence (keyed by the `codex:` account key so it
* never collides with anthropic entries) and does pre-commit rotation only, not
* the full transient-budget / admission machinery.
*/
import { tokenStore } from "../../auth/tokenStore.js";
import { CODEX_ORIGINATOR, CODEX_MODELS_URL, CODEX_RESPONSES_URL, CODEX_USER_AGENT, codexTokenNeedsRefresh, isPermanentCodexRefreshFailure, refreshCodexToken, resolveCodexAccountId, } from "../../auth/codexOAuth.js";
import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from "../../proxy/accountCooldown.js";
import { loadAccountQuotas, saveAccountQuota, } from "../../proxy/accountQuota.js";
import { createCodexUsageTap } from "../../proxy/codexUsage.js";
import { CODEX_ACCOUNT_PREFIX, parseCodexRateLimitHeaders, } from "../../proxy/codexAccountUsage.js";
import { buildClientAttribution } from "../../proxy/clientAttribution.js";
import { getProxyUpstreamFailure } from "../../proxy/proxyFailureDetails.js";
import { registerProxyResponseObserver, isProxyRequestFinalized, registerInternalProxyRequest, getProxyBridgeResult, releaseProxyRequestAccounting, } from "../../proxy/proxyActivity.js";
import { parseCodexNativeRequest, translateCodexRequestToClaude, classifyCodexOutboundFailure, buildSystemBlocksFromDeveloperMessages, } from "../../proxy/codexOutboundFallback.js";
import { createAnthropicFallbackStream } from "../../proxy/codexToAnthropicFallback.js";
import { executeVertexAnthropicFallback } from "../../proxy/vertexAnthropicFallback.js";
import { assertClaudeSystemPrefixShape, assertClaudeMessagesAlternate, codexAnthropicAffinityKey, } from "../../proxy/codexOutboundCache.js";
import { applyClaudeRequestCacheBreakpoints } from "../../utils/anthropicCacheBreakpoints.js";
import { prepareProxyRequestContext, ProxyContextPreflightError, } from "../../proxy/proxyContextPreflight.js";
import { reserveProxyTokenBudget, settleProxyTokenBudget, getProxyTokenBudgetError, getProxyTokenBudgetSessionKey, } from "../../proxy/proxyTokenBudget.js";
import { emitProxyOtelEvent } from "../../proxy/otelLogSink.js";
import { getRuntimeContextWindow, clearRuntimeOutputCeiling, registerRuntimeContextWindow, registerRuntimeOutputCeiling, } from "../../constants/contextWindows.js";
import { logRequest, logRequestAttempt, logBodyCapture, isProxyBodyCaptureEnabled, } from "../../proxy/requestLogger.js";
import { createRawStreamCapture } from "../../proxy/rawStreamCapture.js";
import { resolveProxyLogTraceContext, getProxyRequestTraceContext, } from "../../proxy/proxyTraceContext.js";
import { isBorrowedRequest } from "../../proxy/shareContext.js";
import { ProxyTracer, recordFallbackAttempt } from "../../proxy/proxyTracer.js";
import { parseRetryAfterMs } from "../../proxy/routingPolicy.js";
import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "../../proxy/usageStats.js";
import { raceWithAbort } from "../../utils/async/withTimeout.js";
import { sanitizeForLog } from "../../utils/logSanitize.js";
import { logger } from "../../utils/logger.js";
const CODEX_UPSTREAM_TIMEOUT_MS = 15 * 60 * 1000; // 15 min, matches Claude path
const DEFAULT_TRANSIENT_COOLDOWN_MS = 60_000;
const MAX_TRANSIENT_COOLDOWN_MS = 15 * 60 * 1000;
/** Brief park after a refresh attempt that never reached a verdict. */
const CODEX_AUTH_COOLDOWN_MS = 60_000;
const CODEX_ACCOUNT_TYPE = "codex-oauth";
const CODEX_FALLBACK_METADATA_KEY = "neurolink.codexFallback";
function getCodexTransportErrorCode(error) {
if (!error || typeof error !== "object") {
return undefined;
}
const directCode = error.code;
if (typeof directCode === "string") {
return directCode;
}
const cause = error.cause;
if (!cause || typeof cause !== "object") {
return undefined;
}
const causeCode = cause.code;
return typeof causeCode === "string" ? causeCode : undefined;
}
function codexTransportScope(error) {
const code = getCodexTransportErrorCode(error);
return code === "ENOTFOUND" || code === "EAI_AGAIN"
? "shared_provider_transport"
: "connection_transport";
}
function summarizeCodexUpstreamError(errorText, fallback) {
return sanitizeForLog(errorText).slice(0, 200) || fallback;
}
/**
* In-flight proactive refreshes, keyed by account.
*
* The pool is rebuilt per request with no shared state, so without this every
* concurrent request for the same account fires its own refresh. OpenAI rotates
* the refresh token on each call, so those attempts invalidate one another — the
* losers then see a rejected grant and, via the 401 path, can disable an account
* that is perfectly healthy.
*/
const codexRefreshInFlight = new Map();
async function refreshCodexTokenOnceWithDependencies(key, refreshToken, store, refresh) {
const existing = codexRefreshInFlight.get(key);
if (existing) {
return existing;
}
const pending = (async () => {
// Re-read the stored token instead of trusting the caller's snapshot. A
// request that captured the pool just before a previous refresh completed
// holds a token that has since been rotated; using it would spend a real
// attempt on a grant the server has already invalidated.
const latest = await store.peekTokens(key).catch(() => null);
const current = latest?.refreshToken ?? refreshToken;
const refreshed = await refresh(current);
const resolved = {
accessToken: refreshed.accessToken,
refreshToken: refreshed.refreshToken ?? current,
expiresAt: refreshed.expiresAt ?? latest?.expiresAt ?? Date.now() + 3_600_000,
};
// Hold the single-flight slot through persistence. Releasing it after the
// OAuth response but before this write lets a third request read the old
// rotating refresh token, receive an invalid_grant, and disable an account
// another request has already healed.
if (latest) {
await store.saveTokens(key, {
...resolved,
tokenType: "Bearer",
...(latest.scope ? { scope: latest.scope } : {}),
});
}
return resolved;
})().finally(() => {
codexRefreshInFlight.delete(key);
});
codexRefreshInFlight.set(key, pending);
return pending;
}
/** Refresh an account's token at most once at a time. */
async function refreshCodexTokenOnce(key, refreshToken) {
return refreshCodexTokenOnceWithDependencies(key, refreshToken, tokenStore, refreshCodexToken);
}
// Headers we never forward upstream (hop-by-hop, client creds, or things we
// re-derive). The client's own auth is replaced with the pooled account's.
const BLOCKED_UPSTREAM_HEADERS = new Set([
"host",
"connection",
"content-length",
"transfer-encoding",
"cookie",
"proxy-authorization",
"authorization",
"x-api-key",
"chatgpt-account-id",
"accept-encoding",
]);
/** Build a Codex error body as a Response with the intended status. */
function buildCodexErrorResponse(status, message, code) {
return new Response(JSON.stringify({
error: {
type: "proxy_error",
message,
...(code ? { code, retryable: false } : {}),
},
}), { status, headers: { "content-type": "application/json" } });
}
/**
* Build a terminal Codex Responses SSE stream for a failure that never reached
* an upstream request (every pooled account is cooling).
* A bare non-2xx status with no body reads to the Codex CLI as a dropped
* connection, so it shows "Reconnecting... waiting for network" forever
* instead of a real error. Emitting a well-formed `response.failed` event —
* the same terminal shape the CLI already parses out of a live stream —
* lets it render the actual failure instead.
*
* The code is what the client keys on: `insufficient_quota` tells it the plan
* is spent, so it is reserved for a pool that is out of quota. Accounts parked
* by a transient auth failure report `server_error` instead.
*/
function buildCodexPoolCoolingResponse(message, retryAfterSec, code) {
const payload = {
type: "response.failed",
response: {
error: {
type: code,
code,
message,
},
},
};
const headers = {
"content-type": "text/event-stream",
};
if (retryAfterSec !== undefined) {
headers["retry-after"] = String(retryAfterSec);
}
return new Response(`event: response.failed\ndata: ${JSON.stringify(payload)}\n\n`, { status: 200, headers });
}
/**
* Load the Codex account pool from the token store, refreshing expired OAuth
* tokens and hydrating cooldown + quota state from disk.
*/
async function loadCodexProxyAccounts() {
const [inventory, cooldowns, quotas] = await Promise.all([
tokenStore.getProviderSnapshot(),
loadAccountCooldowns(),
loadAccountQuotas(),
]);
const now = Date.now();
const accounts = [];
for (const [key, entry] of Object.entries(inventory)) {
if (!key.startsWith(CODEX_ACCOUNT_PREFIX) || entry.disabled) {
continue;
}
const tokens = entry.tokens;
if (!tokens || tokens.tokenType !== "Bearer") {
// Only OAuth (Bearer) accounts can serve the ChatGPT backend.
continue;
}
let accessToken = tokens.accessToken;
let expiresAt = tokens.expiresAt;
if (codexTokenNeedsRefresh(expiresAt) && tokens.refreshToken) {
try {
const refreshed = await refreshCodexTokenOnce(key, tokens.refreshToken);
accessToken = refreshed.accessToken;
expiresAt = refreshed.expiresAt ?? expiresAt;
}
catch (error) {
// Keep the stale token; a 401 upstream will trigger rotation.
logger.debug(`Codex proactive refresh failed for ${key.slice(CODEX_ACCOUNT_PREFIX.length)}: ${error instanceof Error ? error.message : String(error)}`);
}
}
const cooldown = cooldowns[key];
const cooling = cooldown && cooldown.coolingUntil > now;
accounts.push({
key,
label: key.slice(CODEX_ACCOUNT_PREFIX.length) || key,
token: accessToken,
refreshToken: tokens.refreshToken,
expiresAt,
accountId: resolveCodexAccountId(accessToken),
quota: quotas[key],
coolingUntil: cooling ? cooldown.coolingUntil : undefined,
coolingReason: cooldown?.reason,
// Kept even once expired: an account only reaches the request loop when it
// is NOT cooling, so this is the only handle the success path has for
// deleting the spent record. Without it they accumulate forever.
expiredCooldownUntil: cooldown && !cooling ? cooldown.coolingUntil : undefined,
});
}
return accounts;
}
/**
* Order non-cooling accounts ahead of cooling ones, then defer rejected quota.
* Within each group, probe unknown quota first and otherwise fill the least-used
* session so fresh accounts get observed rather than starved.
*/
function orderCodexAccounts(accounts, now) {
return [...accounts].sort((a, b) => {
const aCooling = a.coolingUntil !== undefined && a.coolingUntil > now;
const bCooling = b.coolingUntil !== undefined && b.coolingUntil > now;
if (aCooling !== bCooling) {
return aCooling ? 1 : -1;
}
const aRejected = a.quota?.unifiedStatus === "rejected";
const bRejected = b.quota?.unifiedStatus === "rejected";
if (aRejected !== bRejected) {
return aRejected ? 1 : -1;
}
const aUsed = a.quota && a.quota.sessionStatus !== "unknown" ? a.quota.sessionUsed : -1;
const bUsed = b.quota && b.quota.sessionStatus !== "unknown" ? b.quota.sessionUsed : -1;
return aUsed - bUsed;
});
}
/** Build the upstream request headers, replacing client auth with the account's. */
function buildCodexUpstreamHeaders(clientHeaders, account) {
const headers = {};
for (const [name, value] of Object.entries(clientHeaders)) {
const lower = name.toLowerCase();
if (BLOCKED_UPSTREAM_HEADERS.has(lower)) {
continue;
}
headers[lower] = value;
}
headers.authorization = `Bearer ${account.token}`;
const accountId = resolveCodexAccountId(account.token, account.accountId);
if (accountId) {
headers["chatgpt-account-id"] = accountId;
}
headers["content-type"] = "application/json";
if (!headers["user-agent"]) {
headers["user-agent"] = CODEX_USER_AGENT;
}
if (!headers.originator) {
headers.originator = CODEX_ORIGINATOR;
}
if (!headers.accept) {
headers.accept = "text/event-stream";
}
return headers;
}
/** Extract explicit plan-exhaustion evidence without guessing account-wide scope. */
function parseCodexQuotaError(errorText, now) {
try {
const payload = JSON.parse(errorText);
const error = payload?.error;
if (!error ||
typeof error !== "object" ||
(error.type !== "usage_limit_reached" &&
error.code !== "usage_limit_reached")) {
return null;
}
const absolute = error.resets_at ?? error.reset_at;
const relative = error.resets_in_seconds ?? error.reset_after_seconds;
const resetAt = typeof absolute === "number" && Number.isFinite(absolute) && absolute > 0
? absolute > 4_102_444_800
? absolute
: absolute * 1000
: typeof relative === "number" &&
Number.isFinite(relative) &&
relative > 0
? now + relative * 1000
: 0;
// Only explicit account-wide window evidence permits a long cooldown.
// A model/surface-scoped or unscoped rejection must not park the entire pool.
const window = error.limit_type;
const scoped = error.model || (error.scope !== undefined && error.scope !== "account");
const scope = scoped
? "unknown"
: window === "primary" || window === "session"
? "session"
: window === "secondary" || window === "weekly"
? "weekly"
: "unknown";
return {
errorCode: "usage_limit_reached",
resetAt: Number.isFinite(resetAt) ? resetAt : 0,
scope,
};
}
catch {
return null;
}
}
/** Honor known quota resets while bounding unscoped or transient account cooldowns. */
function planCodexCooldown(quota, retryAfterMs, now, errorText = "") {
// A reported reset can already be in the past — a stale header, or a clock
// skew. Taken literally the account is eligible again immediately and the
// pool re-sends to something the provider just rejected.
const floor = now + DEFAULT_TRANSIENT_COOLDOWN_MS;
if (quota) {
if (quota.weeklyStatus === "rejected" && quota.weeklyResetAt > 0) {
return {
coolingUntil: Math.max(quota.weeklyResetAt * 1000, floor),
reason: "weekly",
};
}
if (quota.sessionStatus === "rejected" && quota.sessionResetAt > 0) {
return {
coolingUntil: Math.max(quota.sessionResetAt * 1000, floor),
reason: "session",
};
}
}
const bodyQuota = parseCodexQuotaError(errorText, now);
if (bodyQuota) {
if (bodyQuota.scope !== "unknown" && bodyQuota.resetAt > 0) {
return {
coolingUntil: Math.max(bodyQuota.resetAt, floor),
reason: bodyQuota.scope,
};
}
return {
coolingUntil: now +
Math.min(MAX_TRANSIENT_COOLDOWN_MS, Math.max(bodyQuota.resetAt - now, retryAfterMs, DEFAULT_TRANSIENT_COOLDOWN_MS)),
reason: "unified",
};
}
const delay = Math.min(MAX_TRANSIENT_COOLDOWN_MS, Math.max(retryAfterMs, DEFAULT_TRANSIENT_COOLDOWN_MS));
return {
coolingUntil: now + delay,
reason: quota?.unifiedStatus === "rejected" ? "unified" : "transient",
};
}
/** Set the x-neurolink-* attribution headers on the context. */
function publishCodexHeaders(ctx, account, attempt, quota) {
if (!ctx.responseHeaders) {
ctx.responseHeaders = {};
}
ctx.responseHeaders["x-neurolink-account"] = account.label;
ctx.responseHeaders["x-neurolink-account-type"] = "codex-oauth";
ctx.responseHeaders["x-neurolink-served-by"] = "codex";
ctx.responseHeaders["x-neurolink-attempt"] = String(attempt);
ctx.responseHeaders["x-neurolink-quota-source"] = quota ? "live" : "none";
if (quota && quota.sessionStatus !== "unknown") {
ctx.responseHeaders["x-neurolink-quota-session-left-pct"] = String(Math.round((1 - quota.sessionUsed) * 100));
}
if (quota && quota.weeklyStatus !== "unknown") {
const remaining = String(Math.round((1 - quota.weeklyUsed) * 100));
ctx.responseHeaders["x-neurolink-quota-weekly-left-pct"] = remaining;
// Preserve the original Codex header for existing clients.
ctx.responseHeaders["x-neurolink-weekly-left-pct"] = remaining;
}
}
// =============================================================================
// CODEX-OUTBOUND FALLBACK DISPATCH (stage-c-trigger.md). When the native
// Codex pool itself fails (no accounts, all cooling, a non-retryable
// transport error, or the account-retry loop exhausts without a terminal
// response), and runtime config has opted in, translate the inbound native
// Codex request to Anthropic wire format and serve it from the Anthropic
// OAuth pool (in-process loopback to this same proxy's `/v1/messages`
// handler) or Vertex's Claude passthrough, then translate the response back
// to Codex wire format so the native Codex CLI client never sees the
// difference.
//
// Owns dispatch/HTTP only. Classification lives in codexOutboundFallback.ts's
// `classifyCodexOutboundFailure` (that file's own header disclaims dispatch
// ownership); translation and the response codec live in
// codexOutboundFallback.ts and codexToAnthropicFallback.ts respectively.
// =============================================================================
/** Set once a real dispatch attempt begins (after every gate has passed,
* before any network call). A single flag — not a per-target counter —
* because MAX_ENGINE_CROSSINGS caps the whole request to one crossing
* regardless of which of the four call sites in `dispatch()` reaches this
* function, and insertion point 3 sits inside a loop that can call it more
* than once before insertion point 4 is ever reached. */
const CODEX_OUTBOUND_FALLBACK_ATTEMPTED_KEY = "neurolink.codexOutboundFallbackAttempted";
/** Bounds the Anthropic loopback until its response headers arrive. It is
* cleared once they do, so a long turn can stream past it. */
const CODEX_OUTBOUND_FALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
let codexOutboundFallbackTimeoutOverrideMs = null;
/** `CodexResponseUsage` -> the shared `UsageContext` shape `tracer.setUsage`
* takes. `cacheReadTokensObserved`/`cacheCreationTokensObserved` are false
* only when the field is genuinely absent (never conflated with an
* observed zero). `inputIncludesCachedTokens` is always true because
* `synthesizeCodexUsage` folds cache reads and cache writes into the Codex
* `input_tokens`, the same OpenAI-style total the native Codex path logs. */
function toUsageContext(usage) {
if (!usage) {
return undefined;
}
const cacheRead = usage.input_tokens_details?.cached_tokens;
const cacheCreate = usage.input_tokens_details?.cache_write_tokens;
const cacheCreate1h = usage.input_tokens_details?.cache_write_1h_tokens;
return {
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
cacheReadTokens: cacheRead ?? 0,
cacheCreationTokens: cacheCreate ?? 0,
...(cacheCreate1h === undefined
? {}
: { cacheCreation1hTokens: cacheCreate1h }),
cacheReadTokensObserved: cacheRead !== undefined,
cacheCreationTokensObserved: cacheCreate !== undefined,
inputIncludesCachedTokens: true,
};
}
/** Preserve absent cache fields in the persisted log while the tracer uses
* zero defaults for arithmetic. */
function outboundFallbackUsageLogFields(usage) {
return {
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
cacheReadTokensObserved: usage.cacheReadTokensObserved === true,
cacheCreationTokensObserved: usage.cacheCreationTokensObserved === true,
...(usage.cacheReadTokensObserved === true
? { cacheReadTokens: usage.cacheReadTokens }
: {}),
...(usage.cacheCreationTokensObserved === true
? {
cacheCreationTokens: usage.cacheCreationTokens,
...(usage.cacheCreation1hTokens !== undefined
? { cacheCreation1hTokens: usage.cacheCreation1hTokens }
: {}),
}
: {}),
};
}
/**
* Drive a `CodexResponseStream`'s `frames` generator into a `Response` body,
* calling `onSettled` exactly once with the outcome: the terminal envelope on
* a normal finish, the thrown error on the (never expected, still handled)
* post-commit failure, or `cancelled` when the consumer cancels the
* `ReadableStream` first (client disconnect, or an upstream timeout/abort).
*
* `createAnthropicFallbackStream`'s generator never throws once past its own
* internal preflight (codexToAnthropicFallback.ts:220-517) — every
* post-commit failure is instead serialized as a `response.failed` frame and
* the generator completes normally — so the `catch` branch here is a
* defensive backstop, not the documented failure path.
*/
function wrapCodexOutboundResponseStream(codecStream, headers, onSettled) {
let settled = false;
const settleOnce = (result) => {
if (settled) {
return;
}
settled = true;
onSettled(result);
};
const encoder = new TextEncoder();
// One frame per pull(), so a slow client holds the upstream back instead of
// buffering the whole turn. `cancelled` is set before the codec is
// cancelled: a read that settles after a disconnect must not be recorded
// as a stream error.
let cancelled = false;
const body = new ReadableStream({
async pull(controller) {
try {
const next = await codecStream.frames.next();
if (cancelled) {
return;
}
if (next.done === true) {
controller.close();
settleOnce({ kind: "completed", envelope: next.value });
return;
}
controller.enqueue(encoder.encode(next.value));
}
catch (error) {
if (cancelled) {
return;
}
try {
controller.error(error);
}
catch {
// Already closed or errored.
}
settleOnce({ kind: "error", error });
}
},
cancel(reason) {
cancelled = true;
return codecStream.cancel(reason).finally(() => {
settleOnce({ kind: "cancelled" });
});
},
});
return new Response(body, { status: 200, headers });
}
/** Set the response headers for a served-outbound-fallback turn. Mirrors
* `publishCodexHeaders`'s use of `ctx.responseHeaders`, direction-prefixed
* so it is never confused with a native Codex-pool response. */
function buildCodexOutboundFallbackResponseHeaders(ctx, provider) {
if (!ctx.responseHeaders) {
ctx.responseHeaders = {};
}
ctx.responseHeaders["x-neurolink-served-by"] =
`codex-outbound-fallback:${provider}`;
return {
"content-type": "text/event-stream",
"cache-control": "no-cache",
connection: "keep-alive",
...ctx.responseHeaders,
};
}
/** Forward headers for the loopback `/v1/messages` call — the same allow-list
* `openaiProxyRoutes.ts`'s `buildBridgeHeaders` uses — plus the unforgeable
* internal-origin marker `claudeProxyRoutes.ts` cross-checks against a
* server-set accounting scope that no inbound request header can forge. */
function buildCodexOutboundLoopbackHeaders(ctx, token, affinityKey) {
const forwardHeaders = {
"content-type": "application/json",
accept: "text/event-stream",
"x-neurolink-internal-request": token,
"x-neurolink-internal-origin": "codex-outbound-fallback",
// Ruling 3: derive the Codex-side session-affinity key and forward it as
// the same header claudeProxyRoutes.ts already reads for a native Claude
// client's session id, so the existing (config-gated, off-by-default on
// the live proxy) session-affinity machinery can bind consecutive turns
// of the same Codex conversation to the same Anthropic account.
...(affinityKey ? { "x-claude-code-session-id": affinityKey } : {}),
};
const traceContext = getProxyRequestTraceContext(ctx.requestId);
if (traceContext) {
forwardHeaders.traceparent = `00-${traceContext.traceId}-${traceContext.spanId}-${traceContext.traceFlags.toString(16).padStart(2, "0")}`;
}
for (const [k, v] of Object.entries(ctx.headers)) {
if (typeof v !== "string") {
continue;
}
const lower = k.toLowerCase();
if (["user-agent", "tracestate", "baggage"].includes(lower) ||
(lower === "traceparent" && !forwardHeaders.traceparent)) {
forwardHeaders[lower] = v;
}
}
return forwardHeaders;
}
/**
* One target attempt: dispatch a translated `ClaudeRequest` to either the
* in-process Anthropic-pool loopback or Vertex's Claude passthrough, and
* hand back an Anthropic-shaped `Response` (never a Codex-shape one). Both
* legs are unified through the same `createAnthropicFallbackStream` call one
* level up, so this function's only job per target is "get the Response
* plus a way to attribute it afterward" — Vertex's own `onTerminal` usage
* path is deliberately unused, since usage is read from the shared terminal
* envelope instead (a scope-bounding ruling, not an oversight).
*
* `finalizeAttribution`/`releaseInternal` are split from the dispatch itself
* because, for the anthropic target, the loopback's own account/model
* attribution is only known once its lifecycle completes — which, like the
* OpenAI->Anthropic bridge's `writeLifecycle`, is no earlier than this
* caller's own read of the response body. The caller invokes
* `finalizeAttribution` then `releaseInternal`, in that order, exactly once.
*/
async function dispatchCodexOutboundTarget(args) {
const { ctx, target, claudeRequest, loopbackPort, internalDispatch, affinityKey, } = args;
if (target.provider === "vertex") {
try {
const vertexBody = {
...claudeRequest,
stream: true,
};
const response = await executeVertexAnthropicFallback({
body: vertexBody,
model: target.model,
...(ctx.abortSignal ? { signal: ctx.abortSignal } : {}),
});
return {
ok: true,
response,
// Vertex has no OAuth-pool "account" concept, but it does have a
// model, so it uses the same `vertex/${model}` / "vertex" labeling
// convention as the existing Claude->Vertex fallback leg
// (claudeProxyRoutes.ts's setServedAccount(`vertex/${vertexModel}`,
// "vertex")), keeping per-model cost attribution intact instead of
// collapsing every Codex-outbound-to-Vertex model into one sentinel.
finalizeAttribution: () => ({
account: `vertex/${target.model}`,
accountKey: `vertex/${target.model}`,
accountType: "vertex",
provider: "vertex",
model: target.model,
}),
releaseInternal: () => {
/* Vertex holds no internal-request capability to release. */
},
};
}
catch (error) {
return { ok: false, error };
}
}
// provider === "anthropic": in-process loopback to this same proxy's own
// /v1/messages handler. SECURITY: the target is always 127.0.0.1 on the
// listener's own port, never derived from a client-controlled Host header.
if (!loopbackPort) {
return {
ok: false,
error: new Error("codex-outbound-fallback: no loopback port configured for the anthropic target"),
};
}
// Registration throws at the internal-dispatch capacity limit. That is a
// failed target like any other: the caller moves on to the next target or
// returns the native Codex error, instead of the throw escaping as a 502.
let internal;
try {
internal = registerInternalProxyRequest(ctx.requestId);
}
catch (error) {
return { ok: false, error };
}
const cancellation = new AbortController();
const headersTimeout = setTimeout(() => cancellation.abort(new DOMException("Codex outbound fallback timed out", "TimeoutError")), codexOutboundFallbackTimeoutOverrideMs ??
CODEX_OUTBOUND_FALLBACK_TIMEOUT_MS);
const signal = AbortSignal.any([
cancellation.signal,
...(ctx.abortSignal ? [ctx.abortSignal] : []),
]);
const internalUrl = `http://127.0.0.1:${loopbackPort}/v1/messages`;
let disposed = false;
const finalizeAttribution = () => {
if (!disposed) {
disposed = true;
internal.dispose();
}
const child = getProxyBridgeResult(ctx.requestId);
return {
account: child?.account ?? "anthropic-outbound",
...(child?.accountKey ? { accountKey: child.accountKey } : {}),
accountType: child?.accountType ?? "anthropic-oauth",
provider: child?.provider ?? "anthropic",
model: child?.model ?? target.model,
};
};
const releaseInternal = () => {
if (!disposed) {
disposed = true;
internal.dispose();
}
releaseProxyRequestAccounting(ctx.requestId);
};
try {
const forwardHeaders = buildCodexOutboundLoopbackHeaders(ctx, internal.token, affinityKey);
const requestOptions = {
method: "POST",
headers: forwardHeaders,
body: JSON.stringify({ ...claudeRequest, stream: true }),
signal,
};
const response = await raceWithAbort(Promise.resolve(internalDispatch
? internalDispatch(new Request(internalUrl, requestOptions))
: fetch(internalUrl, requestOptions)), signal);
if (!response.ok) {
const detail = await response.text().catch(() => "");
clearTimeout(headersTimeout);
releaseInternal();
return {
ok: false,
error: new Error(`codex-outbound-fallback: anthropic loopback responded ${response.status}: ${detail.slice(0, 300)}`),
};
}
clearTimeout(headersTimeout);
return {
ok: true,
response,
finalizeAttribution,
releaseInternal,
childRequestId: internal.requestId,
};
}
catch (error) {
clearTimeout(headersTimeout);
releaseInternal();
return { ok: false, error };
}
}
/**
* Attempt the Codex-outbound fallback for one native-Codex-route failure.
* Called from each of `executeCodexResponsesRequest`'s four insertion points
* with a `CodexOutboundFailureInput` describing what that call site observed.
*
* Gating order: already-a-fallback-request -> already-attempted-this-request
* -> classifier eligibility -> runtime-config enabled+targets -> the inbound
* body parses as a native Codex request. Any gate failing returns
* `not_attempted`, and the caller's existing code at that insertion point
* runs completely unchanged — this is what makes the flag-off path byte-
* identical to before this feature existed.
*/
async function attemptCodexOutboundFallback(args) {
const { ctx, body, model, isFallbackRequest, runtimeConfigProvider, loopbackPort, internalDispatch, tracer, recordFinalOutcome, failureInput, } = args;
// A request that itself arrived as an inner fallback leg (the existing
// Claude->Codex direction's own Codex call) must never be offered a
// further outbound fallback — that would be a second engine crossing in
// the same turn, exactly what MAX_ENGINE_CROSSINGS bounds against.
if (isFallbackRequest) {
return { kind: "not_attempted" };
}
if (ctx.metadata?.[CODEX_OUTBOUND_FALLBACK_ATTEMPTED_KEY] === true) {
return { kind: "not_attempted" };
}
if (!runtimeConfigProvider) {
return { kind: "not_attempted" };
}
const decision = classifyCodexOutboundFailure(failureInput);
if (!decision.eligible) {
return { kind: "not_attempted" };
}
let snapshot;
try {
snapshot = runtimeConfigProvider();
}
catch {
return { kind: "not_attempted" };
}
if (!snapshot.codexOutboundFallbackEnabled) {
return { kind: "not_attempted" };
}
const targets = snapshot.codexOutboundFallbackTargets;
if (!targets || targets.length === 0) {
return { kind: "not_attempted" };
}
const parsed = parseCodexNativeRequest(body);
if (!parsed.ok) {
return { kind: "not_attempted" };
}
// Every gate has passed and at least one target will be tried below: this
// is the one dispatch attempt MAX_ENGINE_CROSSINGS allows for this whole
// request, so no later insertion-point call may try again even if every
// target below ultimately fails.
ctx.metadata[CODEX_OUTBOUND_FALLBACK_ATTEMPTED_KEY] = true;
for (const target of targets) {
const mapping = snapshot.codexOutboundFallbackModelMappings.find((m) => m.from === parsed.value.model && m.provider === target.provider);
const resolvedModel = mapping ? mapping.to : target.model;
const translated = translateCodexRequestToClaude(parsed.value, {
provider: target.provider,
model: resolvedModel,
});
if (translated.ok === false) {
if (translated.error.code === "REQUEST_TOO_LARGE") {
// Target-independent: every other configured target would fail the
// exact same way, so this short-circuits the whole loop.
return {
kind: "request_too_large",
message: translated.error.message,
};
}
recordFallbackAttempt({
provider: target.provider,
model: resolvedModel,
status: "failure",
errorMessage: translated.error.message,
durationMs: 0,
direction: "codex-outbound",
});
continue;
}
// Cache preservation (spec cache section; Ruling #3): guard the
// translated request's shape, then mark cache breakpoints with a 1h TTL
// before dispatch. The fallback exists to rescue the turn, so a
// cache-shape problem must never fail it — a thrown guard is logged
// (never with payload content) and the unmodified translated request is
// dispatched without the added breakpoints instead.
let claudeRequest = translated.value;
try {
assertClaudeSystemPrefixShape(translated.value.system, buildSystemBlocksFromDeveloperMessages(parsed.value.input).length);
assertClaudeMessagesAlternate(translated.value.messages);
claudeRequest = applyClaudeRequestCacheBreakpoints(translated.value, {
ttl: "1h",
});
}
catch (error) {
logger.warn(`[codex-outbound-fallback] cache-preservation guard rejected the translated request shape (${target.provider}/${resolvedModel}); dispatching without added cache breakpoints: ${error instanceof Error ? error.name : "unknown error"}`);
}
// Ruling #3: derive the Codex-side session-affinity key so consecutive
// turns of the same Codex conversation can route to the same Anthropic
// account. Only the anthropic target has a session-affinity seam to feed;
// Vertex has none.
const affinityKey = target.provider === "anthropic"
? codexAnthropicAffinityKey(parsed.value)
: undefined;
const attemptStartedAt = Date.now();
const dispatched = await dispatchCodexOutboundTarget({
ctx,
target: { provider: target.provider, model: resolvedModel },
claudeRequest,
loopbackPort,
internalDispatch,
affinityKey,
});
if (dispatched.ok === false) {
recordFallbackAttempt({
provider: target.provider,
model: resolvedModel,
status: "failure",
errorMessage: dispatched.error instanceof Error
? dispatched.error.message
: String(dispatched.error),
durationMs: Date.now() - attemptStartedAt,
direction: "codex-outbound",
});
continue;
}
let codecStream;
try {
codecStream = await createAnthropicFallbackStream(dispatched.response, resolvedModel, translated.toolKindByName);
}
catch (error) {
// Pre-commit failure (createAnthropicFallbackStream's own preflight,
// codexToAnthropicFallback.ts:220-517): safe to try the next target.
recordFallbackAttempt({
provider: target.provider,
model: resolvedModel,
status: "failure",
errorMessage: error instanceof Error ? error.message : String(error),
durationMs: Date.now() - attemptStartedAt,
direction: "codex-outbound",
});
dispatched.releaseInternal();
continue;
}
// COMMIT POINT: createAnthropicFallbackStream resolved, so this leg
// never throws again past this point — every later failure is
// serialized into the stream itself. The leg-health record fires now,
// and this function unconditionally returns success from here on,
// never falling back to try a different target.
//
// Spec: "Call tracer.setRequestOrigin('codex-fallback') at the point the
// new outbound path enters the Anthropic dispatch." Set only here, once a
// target has actually been dispatched and will serve the turn, so a
// request that exhausts every target and ends as a native Codex error
// keeps its root span's origin at "native" rather than being mutated on
// an attempt that was later abandoned.
tracer?.setRequestOrigin("codex-fallback");
recordFallbackAttempt({
provider: target.provider,
model: resolvedModel,
status: "success",
durationMs: Date.now() - attemptStartedAt,
direction: "codex-outbound",
});
const responseHeaders = buildCodexOutboundFallbackResponseHeaders(ctx, target.provider);
const response = wrapCodexOutboundResponseStream(codecStream, responseHeaders, (result) => {
const served = dispatched.finalizeAttribution();
const { account, accountType } = served;
// Read the child's requestId BEFORE releasing: this mirrors
// openaiProxyRoutes.ts's own internal loopback exactly. The anthropic
// target's /v1/messages call logs its own final entry, carrying real
// usage, under `dispatched.childRequestId` as ITS `requestId`. Naming
// it here as this (outer) entry's `usageOwnerRequestId` is what makes
// `proxyAnalysis.ts`'s billing pass skip the outer entry and count the
// upstream call exactly once, via the inner entry — set unconditionally
// (every outcome), since a cancelled/errored outer turn does not
// un-happen the inner call the loopback already made.
// `requestedModel` stays the Codex model writeFinalLog sets; these
// override the Codex-pool defaults so a Vertex-served turn is priced
// as the Claude model on Vertex, not as an OpenAI model.
const accountingExtra = {
...served,
...(dispatched.childRequestId
? {
accountingScope: "client",
usageOwnerRequestId: dispatched.childRequestId,
}
: {}),
};
dispatched.releaseInternal();
try {
if (result.kind === "completed") {
tracer?.setModelSubstitution(model, resolvedModel, target.provider);
tracer?.setServedAccount(account, accountType);
const usage = toUsageContext(result.envelope.usage);
// A loopback child records this call's tokens and cost itself, so
// this outer entry carries neither, as the OpenAI bridge's does.
const delegated = dispatched.childRequestId !== undefined;
if (usage) {
tracer?.setUsage(usage, delegated ? { recordMetrics: false } : undefined);
}
// A failed stream is a failed turn (502), as on the native path.
const failed = result.envelope.status === "failed";
void recordFinalOutcome(undefined, failed ? 502 : 200, {
...accountingExtra,
terminalOutcome: failed ? "stream_error" : "completed",
...(failed
? {
errorType: "outbound_fallback_stream_failed",
errorMessage: result.envelope.error?.message ??
"codex-outbound fallback stream ended in failure",
}
: {}),
...(usage && !delegated
? outboundFallbackUsageLogFields(usage)
: {}),
});
}
else if (result.kind === "cancelled") {
tracer?.setServedAccount(account, accountType);
void recordFinalOutcome(undefined, 499, {
...accountingExtra,
errorType: "client_cancelled",
errorMessage: "Client cancelled Codex request",
terminalOutcome: "client_cancelled",
});
}
else {
tracer?.setServedAccount(account, accountType);
void recordFinalOutcome(undefined, 502, {
...accountingExtra,
errorType: "outbound_fallback_stream_error",
errorMessage: result.error instanceof Error
? result.error.message
: String(result.error),
terminalOutcome: "stream_error",
});
}
}
catch {
// Accounting must never throw across the response boundary.
}
});
return { kind: "success", response };
}
return { kind: "not_attempted" };
}
/** Core pooled handler for POST /backend-api/codex/responses. */
export async function handleCodexResponsesRequest(ctx, runtimeConfigProvider, loopbackPort, internalDispatch) {
if (ctx.metadata?.[CODEX_FALLBACK_METADATA_KEY] !== true) {
void logBodyCapture({
timestamp: new Date().toISOString(),
requestId: ctx.requestId,
phase: "client_request",
model: codexCaptureModel(ctx),
stream: true,
headers: ctx.headers,
body: ctx.body,
contentType: "application/json",
...resolveProxyLogTraceContext({ requestId: ctx.requestId }),
});
}
const response = await executeCodexResponsesRequest(ctx, runtimeConfigProvider, loopbackPort, internalDispatch);
return ctx.metadata?.[CODEX_FALLBACK_METADATA_KEY] === true
? response
: captureCodexResponse(ctx, response, "client_response");
}
function codexCaptureModel(ctx) {
const body = ctx.body;
return typeof body?.model === "string" ? body.model : "-";
}
/** Observe bytes through the existing bounded pass-through; cancellation still reaches upstream. */
function captureCodexResponse(ctx, response, phase, account, attempt) {
if (!isProxyBodyCaptureEnabled()) {
return response;
}
const metadata = {
timestamp: new Date().toISOString(),
requestId: ctx.requestId,
phase,
model: codexCaptureModel(ctx),
stream: true,
headers: Object.fromEntries(response.headers.entries()),
contentType: response.headers.get("content-type") ?? undefined,
responseStatus: response.status,
account: account?.label,
accountType: account ? CODEX_ACCOUNT_TYPE : undefined,
attempt,
...resolveProxyLogTraceContext({ requestId: ctx.requestId }),
};
if (!response.body || isBorrowedRequest()) {
void logBodyCapture(metadata);
return response;
}
const observed = createRawStreamCapture();
void observed.capture
.then((capture) => logBodyCapture({
...metadata,
timestamp: new Date().toISOString(),
body: capture.text,
bodySize: capture.totalBytes,
sourceTruncated: capture.truncated,
}))
.catch(() => undefined);
return new Response(response.body.pipeThrough(observed.stream), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
}
/**
* Serve one native `/backend-api/codex/responses` turn.
*
* Owns the whole hop: account selection and rotation, the upstream request,
* the SSE relay back to the caller, and the terminal accounting that turns the
* observed wire usage into request-log and tracer records. Usage counts the
* provider did not report are carried as unobserved rather than zero, so a
* silent cache breakdown is never recorded as a cache miss.
*/
async function executeCodexResponsesRequest(ctx,
// Read by the outbound-fallback insertion points below (stage-c-trigger.md)
// to gate/target a Codex-outbound fallback attempt when the native Codex
// pool itself fails.
runtimeConfigProvider,
// Threaded through from `createCodexProxyRoutes` to the anthropic-target
// loopback leg of the outbound-fallback dispatcher (mirrors the
// OpenAI->Anthropic bridge's `loopbackPort`/`internalDispatch`).
loopbackPort, internalDispatch) {
const requestStartTime = Date.now();
let body = (ctx.body ?? {});
let bodyStr;
let preparedContext;
let contextPreflight;
let budgetLease;
let budgetDispatched = false;
const settleBudget = async (actualTokens) => {
if (!budgetLease) {
return;
}
try {
if (budgetDispatched) {
await settleProxyTokenBudget(budgetLease, actualTokens);
if (budgetLease.snapshot.settlement === "unconfirmed") {
emitProxyOtelEvent("token_budget", {
requestId: ctx.requestId,
event: "settlement_unconfirmed",
});
}
}
else {
await budgetLease.cancelBeforeDispatch();
}
}
catch (error) {
emitProxyOtelEvent("token_budget", {
requestId: ctx.requestId,
event: "settlement_unconfirmed",
errorCode: getProxyTokenBudgetError(error)?.code ??
"PROXY_TOKEN_BUDGET_UNAVAILABLE",
});
}
};
const model = typeof body.model === "string"
? body.model
: "-";
// A Codex call made as an inner Anthropic fallback is an upstream attempt,
// not an independently final client request. The parent fallback owns the
// final status and can still recover with a later provider.
const isFallbackRequest = ctx.metadata?.[CODEX_FALLBACK_METADATA_KEY] === true;
const reasoning = body.reasoning;
const reasoningEffort = reasoning &&
typeof reasoning === "object" &&
"effort" in reasoning &&
typeof reasoning.effort === "string"
? reasoning.effort
: undefined;
let tracer;
try {
tracer = ProxyTracer.startRequest({
requestId: ctx.requestId,
method: ctx.method,
path: ctx.path,
model,
stream: true,
toolCount: Array.isArray(body.tools)
? body.tools.length
: 0,
provider: "openai",
userAgent: ctx.headers["user-agent"],
recordRequestMetrics: !isFallbackRequest,
recordUsageMetrics: true,
}, ctx.headers);
}
catch {
// Instrumentation must not change provider request handling.
}
const writeFinalLog = (account, responseStatus, extra = {}) => logRequest({
timestamp: new Date().toISOString(),
requestId: ctx.requestId,
method: ctx.method,
path: ctx.path,
model,
requestedModel: model,
contextPreflight,
tokenBudget: budgetLease ? { ...budgetLease.snapshot } : undefined,
stream: true,
toolCount: Array.isArray(body.tools)
? body.tools.length
: 0,
account: account?.label ?? "",
...(account ? { accountKey: account.key } : {}),
accountType: account ? CODEX_ACCOUNT_TYPE : "",
// This is the cost provider. accountKey and the response header identify
// the actual Codex pool that supplied the credential.
provider: "openai",
inputIncludesCachedTokens: true,
...tracer?.getTraceContext(),
...(reasoningEffort ? { reasoningEffort } : {}),
firstUsefulOutputStatus: extra.firstUsefulOutputMs !== undefined ? "observed" : "not_observed",
...buildClientAttribution(ctx.headers),
responseStatus,
responseTimeMs: Date.now() - requestStartTime,
...extra,
});
let finalOutcomeRecorded = false;
const recordFinalOutcome = async (account, responseStatus, extra = {}) => {
await settleBudget(extra.inputTokens !== undefined && extra.outputTokens !== undefined
? extra.inputTokens + extra.outputTokens
: undefined);
if (finalOutcomeRecorded) {
return;
}
finalOutcomeRecorded = true;
if (isProxyRequestFinalized(ctx.requestId)) {
// Runtime owns the client final, but this route still owns its span.
const status = ctx.abortSignal?.aborted
? ctx.abortSignal.reason?.name === "TimeoutError"
? 504
: 499
: responseStatus;
try {
tracer?.end(status, Date.now() - requestStartTime);
}
catch {
/* best effort */
}
return;
}
try {
if (extra.errorType) {
tracer?.setError(extra.errorType, extra.errorMessage ?? extra.errorType);
}
tracer?.end(responseStatus, Date.now() - requestStartTime);
}
catch {
// End bookkeeping is best effort; the client outcome remains authoritative.
}
if (isFallbackRequest) {
return;
}
if (responseStatus >= 400) {
recordFinalError(responseStatus, account?.label, account ? CODEX_ACCOUNT_TYPE : undefined, {
requestId: ctx.requestId,
...(account ? { accountKey: account.key } : {}),
errorType: extra.errorType,
terminalOutcome: extra.terminalOutcome ?? "handler_error",
message: extra.errorMessage,
errorCode: extra.errorCode,
});
}
else {
recordFinalSuccess(account?.label, account ? CODEX_ACCOUNT_TYPE : undefined);
}
await writeFinalLog(account, responseStatus, extra);
};
const writeAttempt = (account, attempt, startedAt, responseStatus, extra = {}) => {
void logRequestAttempt({
timestamp: new Date().toISOString(),
requestId: ctx.requestId,
attempt,
...tracer?.getTraceContext(),
...(isFallbackRequest
? { parentRequestId: ctx.requestId.replace(/:codex-fallback$/, "") }
: {}),
...(reasoningEffort ? { reasoningEffort } : {}),
method: ctx.method,
path: ctx.path,
model,
requestedModel: model,
contextPreflight,
tokenBudget: budgetLease ? { ...budgetLease.snapshot } : undefined,
stream: true,
toolCount: Array.isArray(body.tools)
? body.tools.length
: 0,
account: account.label,
accountKey: account.key,
accountType: CODEX_ACCOUNT_TYPE,
provider: "openai",
responseStatus,
responseTimeMs: Date.now() - requestStartTime,
attemptDurationMs: Date.now() - startedAt,
...extra,
}).catch(() => undefined);
};
const dispatch = async () => {
try {
preparedContext = prepareProxyRequestContext({
provider: "codex",
model,
body,
});
contextPreflight = preparedContext.evidence;
body = preparedContext.body;
bodyStr = JSON.stringify(body);
}
catch (error) {
if (!(error instanceof ProxyContextPreflightError)) {
throw error;
}
contextPreflight = error.evidence;
await recordFinalOutcome(undefined, error.status, {
errorType: "context_preflight",
errorCode: error.code,
errorMessage: error.message,
retryable: false,
terminalOutcome: "handler_error",
});
return buildCodexErrorResponse(error.status, error.message, error.code);
}
const accounts = await loadCodexProxyAccounts();
const cancelRequest = async (account) => {
await recordFinalOutcome(account, 499, {
errorType: "client_cancelled",
errorMessage: "Client cancelled Codex request",
terminalOutcome: "client_cancelled",
});
return buildCodexErrorResponse(499, "Client cancelled Codex request");
};
if (ctx.abortSignal?.aborted) {
return cancelRequest();
}
if (accounts.length === 0) {
const outbound = await attemptCodexOutboundFallback({
ctx,
body,
model,
isFallbackRequest,
runtimeConfigProvider,
loopbackPort,
internalDispatch,
tracer,
recordFinalOutcome,
failureInput: { failureClass: "no_accounts" },
});
if (outbound.kind === "success") {
// No Codex account was ever selected on this path, so budgetLease is
// always undefined here — settleBudget() is a defensive no-op. Kept
// for consistency with the other three insertion points below.
await settleBudget();
budgetLease = undefined;
budgetDispatched = false;
return outbound.response;
}
if (outbound.kind === "request_too_large") {
await recordFinalOutcome(undefined, 413, {
errorType: "request_too_large",
errorMessage: outbound.message,
terminalOutcome: "handler_error",
});
return buildCodexErrorResponse(413, outbound.message, "request_too_large");
}
await recordFinalOutcome(undefined, 401, {
errorType: "no_accounts",
errorMessage: "No Codex accounts",
});
return buildCodexErrorResponse(401, "No Codex accounts configured. Run `neurolink auth login codex`.");
}
const now = Date.now();
const ordered = orderCodexAccounts(accounts, now);
const eligible = ordered.filter((a) => !(a.coolingUntil !== undefined && a.coolingUntil > now));
if (eligible.length === 0) {
// Every account is cooling; surface the soonest recovery as retry-after.
const soonest = ordered.reduce((min, a) => {
if (a.coolingUntil === undefined) {
return min;
}
return min === undefined
? a.coolingUntil
: Math.min(min, a.coolingUntil);
}, undefined);
const retryAfterSec = soonest
? Math.max(1, Math.ceil((soonest - now) / 1000))
: 60;
const outbound = await attemptCodexOutboundFallback({
ctx,
body,
model,
isFallbackRequest,
runtimeConfigProvider,
loopbackPort,
internalDispatch,
tracer,
recordFinalOutcome,
failureInput: { failureClass: "pool_exhausted" },
});
if (outbound.kind === "success") {
// No Codex account was ever selected on this path, so budgetLease is
// always undefined here — settleBudget() is a defensive no-op. Kept
// for consistency with the other three insertion points below.
await settleBudget();
budgetLease = undefined;
budgetDispatched = false;
return outbound.response;
}
if (outbound.kind === "request_too_large") {
await recordFinalOutcome(undefined, 413, {
errorType: "request_too_large",
errorMessage: outbound.message,
terminalOutcome: "handler_error",
});
return buildCodexErrorResponse(413, outbound.message, "request_too_large");
}
const resetAt = soonest ? new Date(soonest).toISOString() : undefined;
// A refresh that failed transiently, or a 401 with no refresh token to
// retry with, parks the account as "auth". Reporting that as spent quota
// would tell the user the plan is exhausted when it is not.
if (ordered.every((account) => account.coolingReason === "auth")) {
await recordFinalOutcome(undefined, 503, {
errorType: "all_accounts_auth_cooling",
errorMessage: "All Codex accounts are cooling after an auth failure",
});
return buildCodexPoolCoolingResponse(resetAt
? `Codex authentication is temporarily unavailable for every account. Retry after ${resetAt}, or run \`neurolink auth login codex\` if it persists.`
: "Codex authentication is temporarily unavailable for every account. Retry shortly, or run `neurolink auth login codex` if it persists.", retryAfterSec, "server_error");
}
await recordFinalOutcome(undefined, 429, {
errorType: "all_accounts_cooling",
errorMessage: "All Codex accounts are rate-limited",
});
return buildCodexPoolCoolingResponse(resetAt
? `Codex quota exhausted: all accounts are rate-limited. Resets at ${resetAt}.`
: "Codex quota exhausted: all accounts are rate-limited.", retryAfterSec, "insufficient_quota");
}
let attempt = 0;
let lastErrorMessage = "All Codex accounts failed";
let lastErrorStatus = 502;
// Set with every lastErrorStatus so the outbound-fallback classifier sees
// the code of the same attempt, never a stale one from an earlier account.
let lastErrorCode;
let lastFailure = { errorType: "all_accounts_failed" };
let lastAttemptedAccount;
for (const account of eligible) {
try {
tracer?.setAccountSelection({
strategy: "codex-quota-order",
accountsTotal: accounts.length,
accountsHealthy: eligible.length,
selectedAccount: account.label,
accountType: CODEX_ACCOUNT_TYPE,
});
}
catch {
// Account attribution cannot affect routing.
}
let authRetried = false;
// Same-account loop only re-runs once, for a post-401 token refresh.
for (;;) {
if (ctx.abortSignal?.aborted) {
return cancelRequest(lastAttemptedAccount);
}
await settleBudget();
budgetLease = undefined;
budgetDispatched = false;
try {
budgetLease = await reserveProxyTokenBudget({
provider: "codex",
accountKey: account.key,
sessionKey: getProxyTokenBudgetSessionKey(new Headers(ctx.headers)),
requestId: ctx.requestId,
reservationTokens: preparedContext.totalTokensReservation,
estimateProvenance: "estimated_input_plus_serving_model_output_reserve",
});
}
catch (error) {
const budgetError = getProxyTokenBudgetError(error);
if (!budgetError) {
throw error;
}
await recordFinalOutcome(account, budgetError.status, {
errorType: "token_budget",
errorCode: budgetError.code,
errorMessage: budgetError.message,
retryable: false,
terminalOutcome: "handler_error",
});
return buildCodexErrorResponse(budgetError.status, budgetError.message, budgetError.code);
}
if (ctx.abortSignal?.aborted) {
return cancelRequest(account);
}
attempt += 1;
const attemptStartedAt = Date.now();
lastAttemptedAccount = account;
recordAttempt(account.label, CODEX_ACCOUNT_TYPE);
let upstream;
try {
const upstreamHeaders = buildCodexUpstreamHeaders(ctx.headers, account);
void logBodyCapture({
timestamp: new Date().toISOString(),
requestId: ctx.requestId,
phase: "upstream_request",
model,
stream: true,
body: bodyStr,
headers: upstreamHeaders,
contentType: "application/json",
account: account.label,
accountType: CODEX_ACCOUNT_TYPE,
attempt,
...tracer?.getTraceContext(),
});
budgetDispatched = true;
upstream = await fetch(CODEX_RESPONSES_URL, {
method: "POST",
headers: upstreamHeaders,
body: bodyStr,
signal: ctx.abortSignal
? AbortSignal.any([
ctx.abortSignal,
AbortSignal.timeout(CODEX_UPSTREAM_TIMEOUT_MS),
])
: AbortSignal.timeout(CODEX_UPSTREAM_TIMEOUT_MS),
});
}
catch (error) {
if (ctx.abortSignal?.aborted) {
writeAttempt(account, attempt, attemptStartedAt, 499, {
errorType: "client_cancelled",
errorMessage: "Client cancelled Codex request",
retryable: false,
});
return cancelRequest(account);
}
// A transport failure message is derived from local state — resolved
// hostnames, socket paths, Node internals — and says nothing the caller
// can act on. Keep the detail in the log and return a fixed string, so
// internal topology never reaches the client.
logger.debug(`Codex upstream fetch failed (${account.label}): ${sanitizeForLog(error instanceof Error ? error.message : String(error))}`);
const errorMessage = summarizeCodexUpstreamError(error instanceof Error ? error.message : String(error), "Codex upstream request failed");
const errorCode = getCodexTransportErrorCode(error);
const transportScope = codexTransportScope(error);
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, 502);
writeAttempt(account, attempt, attemptStartedAt, 502, {
errorType: "network_error",
errorMessage,
...(errorCode ? { errorCode } : {}),
transportScope,
// These codes prove failure before HTTP dispatch. Socket resets,
// EPIPE and generic timeouts may follow dispatch and must not replay.
retryable: [
"UND_ERR_CONNECT_TIMEOUT",
"ECONNREFUSED",
"ENOTFOUND",
"EAI_AGAIN",
].includes(errorCode ?? ""),
});
lastFailure = {
errorType: "network_error",
errorMessage,
errorCode,
transportScope,
};
if (![
"UND_ERR_CONNECT_TIMEOUT",
"ECONNREFUSED",
"ENOTFOUND",
"EAI_AGAIN",
].includes(errorCode ?? "")) {
const outbound = await attemptCodexOutboundFallback({
ctx,
body,
model,
isFallbackRequest,
runtimeConfigProvider,
loopbackPort,
internalDispatch,
tracer,
recordFinalOutcome,
failureInput: {
failureClass: "non_retryable_transport",
transportErrorCode: errorCode,
},
});
if (outbound.kind === "success") {
// This Codex account's budget lease (reserved for the attempt
// that just failed transport-level) belongs to a request this
// turn is no longer serving via Codex at all — hold it open no
// longer than necessary rather than leaving it pending for the
// unrelated fallback stream's full duration.
await settleBudget();
budgetLease = undefined;
budgetDispatched = false;
return outbound.response;
}
if (outbound.kind === "request_too_large") {
await recordFinalOutcome(account, 413, {
errorType: "request_too_large",
errorMessage: outbound.message,
terminalOutcome: "handler_error",
});
return buildCodexErrorResponse(413, outbound.message, "request_too_large");
}
await recordFinalOutcome(account, 502, {
...lastFailure,
errorMessage,
});
return buildCodexErrorResponse(502, "Codex upstream request failed");
}
lastErrorMessage = "Codex upstream request failed";
lastErrorStatus = 502;
lastErrorCode = lastFailure.errorCode;
break; // rotate to next account
}
upstream = captureCodexResponse(ctx, upstream, "upstream_response", account, attempt);
if (upstream.ok) {
const quota = parseCodexRateLimitHeaders(upstream.headers);
if (quota) {
saveAccountQuota(account.key, quota).catch(() => undefined);
}
// A prior cooldown that has expired is cleared on success. The
// compare-and-swap guards against wiping a longer cooldown that another
// in-flight request set while this one was upstream.
if (account.expiredCooldownUntil !== undefined) {
clearAccountCooldown(account.key, account.expiredCooldownUntil).catch(() => undefined);
}
publishCodexHeaders(ctx, account, attempt, quota);
writeAttempt(account, attempt, attemptStartedAt, upstream.status);
const headers = {
"content-type": upstream.headers.get("content-type") ?? "text/event-stream",
"cache-control": "no-cache",
connection: "keep-alive",
...(ctx.responseHeaders ?? {}),
};
if (!upstream.body) {
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, 502);
writeAttempt(account, attempt, attemptStartedAt, 502, {
errorType: "incomplete_stream",
errorMessage: "Codex returned no response stream",
retryable: false,
});
await recordFinalOutcome(account, 502, {
terminalOutcome: "stream_error",
errorType: "incomplete_stream",
errorMessage: "Codex returned no response stream",
});
return new Response(upstream.body, {
status: upstream.status,
headers,
});
}
const { stream: usageTap, usage: usageSeen, evidence, } = createCodexUsageTap();
const relay = new Response(upstream.body.pipeThrough(usageTap), {
status: upstream.status,
headers,
});
registerProxyResponseObserver(ctx.metadata, {
onTerminal: ({ outcome, error, observedBodyBytes }) => {
return usageSeen
.then((usage) => {
const semantic = evidence();
const completedFrameDelivered = semantic.completed &&
observedBodyBytes >= semantic.terminalBytes;
const failed = semantic.errorType ||
((outcome === "completed" || outcome === "bodyless") &&
!semantic.completed);
const usageExtra = usage
? {
inputTokens: usage.inputTokensObserved
? usage.inputTokens
: undefined,
outputTokens: usage.outputTokensObserved
? usage.outputTokens
: undefined,
cacheReadTokens: usage.cacheReadTokensObserved
? usage.cacheReadTokens
: undefined,
cacheCreationTokens: usage.cacheCreationTokensObserved
? usage.cacheCreationTokens
: undefined,
reasoningTokens: usage.reasoningTokensObserved
? usage.reasoningTokens
: undefined,
}
: {};
if (usage?.inputTokensObserved &&
usage.outputTokensObserved) {
try {
tracer?.setUsage({
...usage,
reasoningTokens: usage.reasoningTokensObserved
? usage.reasoningTokens
: undefined,
cacheReadTokensObserved: usage.cacheReadTokensObserved === true,
cacheCreationTokensObserved: usage.cacheCreationTokensObserved === true,
inputIncludesCachedTokens: true,
});
}
catch {
// Pricing/metrics must never change the stream outcome.
}
}
const timing = semantic.firstUsefulOutputAt === undefined ||
semantic.observationIncomplete
? {
firstUsefulOutputStatus: semantic.completed &&
!semantic.observationIncomplete
? "no_useful_output"
: "not_observed",
}
: {
firstUsefulOutputStatus: "observed",
firstUsefulOutputEvent: semantic.firstUsefulOutputEvent,
firstUsefulOutputMs: Math.max(0, semantic.firstUsefulOutputAt - requestStartTime),
};
if (failed) {
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, 502);
writeAttempt(account, attempt, attemptStartedAt, 502, {
// An internal fallback has no separate final row. Keep
// its observed usage on this attempt when a later
// provider owns the client final.
...usageExtra,
inputIncludesCachedTokens: true,
errorType: semantic.errorType ?? "incomplete_stream",
errorCode: semantic.errorCode,
errorMessage: semantic.errorMessage ??
"Codex stream ended without a completion event",
retryable: false,
});
return recordFinalOutcome(account, 502, {
...usageExtra,
...timing,
terminalOutcome: "stream_error",
errorType: semantic.errorType ?? "incomplete_stream",
errorCode: semantic.errorCode,
errorMessage: semantic.errorMessage ??
"Codex stream ended without a completion event",
});
}
if (outcome === "completed" ||
outcome === "bodyless" ||
(outcome === "client_cancelled" && completedFrameDelivered)) {
return recordFinalOutcome(account, upstream.status, {
terminalOutcome: "completed",
...usageExtra,
...timing,
});
}
if (outcome === "stream_error") {
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, 502);
writeAttempt(account, attempt, attemptStartedAt, 502, {
errorType: "stream_error",
errorCode: getCodexTransportErrorCode(error),
errorMessage: summarizeCodexUpstreamError(error instanceof Error ? error.message : "", "Codex upstream stream failed"),
retryable: false,
});
}
return recordFinalOutcome(account, outcome === "client_cancelled" ? 499 : 502, {
errorType: outcome === "client_cancelled"
? "client_cancelled"
: "stream_error",
errorMessage: outcome === "client_cancelled"
? "Client cancelled Codex stream"
: summarizeCodexUpstreamError(error instanceof Error ? error.message : "", "Codex upstream stream failed"),
...(outcome === "stream_error"
? { errorCode: getCodexTransportErrorCode(error) }
: {}),
terminalOutcome: outcome,
...usageExtra,
...timing,
});
})
.catch(() => undefined);
},
});
return relay;
}
const errText = await upstream.text().catch(() => "");
// 401/403 → try a forced token refresh once, then rotate.
if ((upstream.status === 401 || upstream.status === 403) &&
!authRetried &&
account.refreshToken) {
const errorMessage = summarizeCodexUpstreamError(errText, "Codex authentication rejected upstream");
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, upstream.status);
writeAttempt(account, attempt, attemptStartedAt, upstream.status, {
errorType: "authentication_error",
errorMessage,
retryable: true,
});
authRetried = true;
const staleTokens = {
accessToken: account.token,
refreshToken: account.refreshToken,
expiresAt: account.expiresAt ?? 0,
};
try {
const refreshed = await refreshCodexTokenOnce(account.key, account.refreshToken);
account.token = refreshed.accessToken;
account.refreshToken =
refreshed.refreshToken ?? account.refreshToken;
account.expiresAt = refreshed.expiresAt ?? account.expiresAt;
account.accountId = resolveCodexAccountId(refreshed.accessToken);
continue; // retry same account with the fresh token
}
catch (error) {
if (isPermanentCodexRefreshFailure(error)) {
// Compare-and-swap: the pool is rebuilt per request with no shared
// state, so a concurrent request may already have rotated this
// credential. Disabling unconditionally would kill the account that
// the other request just healed.
const disabled = await tokenStore.markDisabledIfCurrent(account.key, staleTokens, "refresh_invalid");
if (disabled) {
logger.always(`[proxy] codex account=${account.label} disabled until re-authentication. Run: neurolink auth login codex --label ${account.label}`);
}
lastFailure = {
errorType: "authentication_error",
errorCode: "refresh_invalid",
};
lastErrorStatus = 401;
lastErrorCode = lastFailure.errorCode;
lastErrorMessage =
"Codex token refresh failed; re-login required";
break;
}
// No verdict on the credential — cool briefly and try the next
// account, so a 5xx or a timeout cannot cost the user a login.
await saveAccountCooldown(account.key, Date.now() + CODEX_AUTH_COOLDOWN_MS, "auth").catch(() => undefined);
logger.debug(`[proxy] codex account=${account.label} refresh failed transiently; cooling and rotating`);
lastFailure = {
errorType: "auth_refresh_unavailable",
errorCode: getCodexTransportErrorCode(error),
};
lastErrorStatus = 503;
lastErrorCode = lastFailure.errorCode;
lastErrorMessage = "Codex token refresh temporarily unavailable";
break;
}
}
// 429 → cooldown + rotate.
if (upstream.status === 429) {
const quota = parseCodexRateLimitHeaders(upstream.headers);
if (quota) {
saveAccountQuota(account.key, quota).catch(() => undefined);
}
const retryAfterMs = parseRetryAfterMs(upstream.headers.get("retry-after"));
const now = Date.now();
const bodyQuota = parseCodexQuotaError(errText, now);
const plan = planCodexCooldown(quota, retryAfterMs, now, errText);
await saveAccountCooldown(account.key, plan.coolingUntil, plan.reason).catch(() => undefined);
const rateLimitKind = plan.reason === "transient" ? "transient" : "quota";
const errorMessage = summarizeCodexUpstreamError(errText, "Codex account rate-limited");
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, upstream.status, rateLimitKind);
writeAttempt(account, attempt, attemptStartedAt, upstream.status, {
errorType: "rate_limit_error",
errorMessage,
retryable: rateLimitKind === "transient",
rateLimitKind,
cooldownReason: plan.reason,
...(bodyQuota
? {
errorCode: bodyQuota.errorCode,
quotaResetAt: bodyQuota.resetAt || undefined,
quotaScope: bodyQuota.scope,
}
: {}),
});
lastFailure = {
errorType: "rate_limit_error",
errorCode: bodyQuota?.errorCode,
};
lastErrorStatus = 429;
lastErrorCode = lastFailure.errorCode;
lastErrorMessage = "Codex account rate-limited";
break; // rotate
}
// Other non-ok → record and rotate.
if (upstream.status === 401 || upstream.status === 403) {
// Reached only when the account has no refresh token to retry with, so
// it will fail identically on the next request. Park it briefly instead
// of letting it stay first in line with unknown quota.
await saveAccountCooldown(account.key, Date.now() + CODEX_AUTH_COOLDOWN_MS, "auth").catch(() => undefined);
}
const errorMessage = summarizeCodexUpstreamError(errText, "Codex error");
const errorType = upstream.status === 401 || upstream.status === 403
? "authentication_error"
: "api_error";
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, upstream.status);
writeAttempt(account, attempt, attemptStartedAt, upstream.status, {
errorType,
errorMessage,
retryable: upstream.status >= 500,
});
lastFailure = { errorType };
lastErrorStatus = upstream.status >= 500 ? 502 : upstream.status;
lastErrorCode = getProxyUpstreamFailure({
responseBody: errText,
})?.code;
lastErrorMessage = errorMessage;
break; // rotate
}
}
const outbound = await attemptCodexOutboundFallback({
ctx,
body,
model,
isFallbackRequest,
runtimeConfigProvider,
loopbackPort,
internalDispatch,
tracer,
recordFinalOutcome,
failureInput: {
failureClass: "loop_fallthrough",
lastStatus: lastErrorStatus,
lastErrorCode,
},
});
if (outbound.kind === "success") {
// Same reasoning as the transport-catch insertion point above: settle
// the exhausted Codex account's budget lease immediately rather than
// holding it open for the unrelated fallback stream's duration.
await settleBudget();
budgetLease = undefined;
budgetDispatched = false;
return outbound.response;
}
if (outbound.kind === "request_too_large") {
await recordFinalOutcome(lastAttemptedAccount, 413, {
errorType: "request_too_large",
errorMessage: outbound.message,
terminalOutcome: "handler_error",
});
return buildCodexErrorResponse(413, outbound.message, "request_too_large");
}
await recordFinalOutcome(lastAttemptedAccount, lastErrorStatus, {
...lastFailure,
errorMessage: lastFailure.errorMessage ?? lastErrorMessage,
});
return buildCodexErrorResponse(lastErrorStatus, lastErrorMessage);
};
try {
return await dispatch();
}
catch (error) {
await settleBudget();
try {
tracer?.end(502, Date.now() - requestStartTime);
}
catch {
// Shared HTTP error handling owns the client outcome and final log.
}
throw error;
}
}
/** Accept exact positive limits advertised by successful Codex model discovery. */
function registerCodexDiscoveredLimits(body) {
if (Buffer.byteLength(body, "utf8") > 2 * 1024 * 1024) {
return;
}
let parsed;
try {
parsed = JSON.parse(body);
}
catch {
return;
}
if (!parsed ||
typeof parsed !== "object" ||
!("models" in parsed) ||
!Array.isArray(parsed.models)) {
return;
}
for (const candidate of parsed.models.slice(0, 1024)) {
if (!candidate || typeof candidate !== "object") {
continue;
}
const model = candidate.slug ?? candidate.id;
if (typeof model !== "string" || model.length === 0 || model.length > 256) {
continue;
}
const context = candidate.context_window;
const output = candidate.max_output_tokens;
if (Number.isSafeInteger(context) && context > 0) {
registerRuntimeContextWindow("codex", model, context);
}
const knownContext = getRuntimeContextWindow("codex", model);
if (Number.isSafeInteger(output) &&
output > 0 &&
(knownContext === undefined || output < knownContext)) {
registerRuntimeOutputCeiling("codex", model, output);
}
else {
// Rediscovery may shrink the context or stop advertising an output cap.
// Do not preserve a stale ceiling that can make every input invalid.
clearRuntimeOutputCeiling("codex", model);
}
}
}
/**
* Relay Codex model discovery upstream.
*
* The CLI refreshes its model list on every invocation. Only `/responses` was
* registered, so that GET 404'd and the CLI printed a refresh failure before
* falling back to a default model — quietly ignoring the model the user had
* configured.
*
* This relays rather than synthesises, unlike the Claude and OpenAI `/v1/models`
* routes, which build their lists locally from the model router. Codex model
* availability is a property of the upstream account (plan tier, rollout), not
* of anything this proxy knows, so a synthesised list would be a guess that
* looks authoritative.
*
* Read-only with respect to ROUTING: no cooldown is recorded and no quota is
* consumed, so a discovery call cannot influence which account real traffic
* lands on. It is not literally side-effect free — a token refreshed below is
* persisted, exactly as the proactive refresh in `loadCodexProxyAccounts`
* persists one. What discovery deliberately never does is *penalise* an
* account: it cannot cool one and cannot disable one. A read-only probe must
* not be able to cost the user a login.
*/
async function handleCodexModelsRequest(ctx) {
const accounts = await loadCodexProxyAccounts();
if (accounts.length === 0) {
return buildCodexErrorResponse(401, "No Codex accounts configured. Run `neurolink auth login codex`.");
}
const now = Date.now();
const ordered = orderCodexAccounts(accounts, now);
// A cooling account is rate-limited for completions, not barred from
// answering what models exist. Healthy accounts go first, but a cooling one
// is still a candidate rather than a reason to fail discovery outright.
const isCooling = (a) => a.coolingUntil !== undefined && a.coolingUntil > now;
const candidates = [
...ordered.filter((a) => !isCooling(a)),
...ordered.filter(isCooling),
];
// Forward the CLI's own query — it sends client_version, and upstream
// *requires* it: without it ChatGPT answers 400 with a pydantic
// "Field required" on ('query','client_version'). Rebuild from ctx.query,
// not ctx.path: path carries no query string, so reading it there silently
// dropped the parameter and produced exactly that 400.
const params = new URLSearchParams(ctx.query ?? {});
const query = params.toString();
const url = query ? `${CODEX_MODELS_URL}?${query}` : CODEX_MODELS_URL;
let lastErrorStatus = 502;
let lastErrorMessage = "Codex model discovery upstream failed";
for (const account of candidates) {
// One forced refresh per account, then move on. A token can be rejected
// upstream while still inside its local expiry window, so relaying that
// 401 straight back left discovery broken until the token expired locally
// — the CLI would fall back to a default model on every invocation in the
// meantime.
let authRetried = false;
for (;;) {
let upstream;
try {
upstream = await fetch(url, {
method: "GET",
headers: buildCodexUpstreamHeaders(ctx.headers ?? {}, account),
// Bound the upstream call, as the responses route does. Without a
// signal a stalled connection holds the proxy request open with no
// ceiling, and the CLI blocks on model discovery at startup.
signal: AbortSignal.any([
...(ctx.abortSignal ? [ctx.abortSignal] : []),
AbortSignal.timeout(CODEX_UPSTREAM_TIMEOUT_MS),
]),
});
}
catch (error) {
lastErrorStatus = 502;
lastErrorMessage = `Codex model discovery upstream failed: ${error instanceof Error ? error.message : String(error)}`;
break; // rotate to the next account
}
if (upstream.status === 401 || upstream.status === 403) {
if (!authRetried && account.refreshToken) {
authRetried = true;
try {
const refreshed = await refreshCodexTokenOnce(account.key, account.refreshToken);
account.token = refreshed.accessToken;
account.refreshToken =
refreshed.refreshToken ?? account.refreshToken;
account.expiresAt = refreshed.expiresAt ?? account.expiresAt;
account.accountId = resolveCodexAccountId(refreshed.accessToken);
continue; // retry this account with the fresh token
}
catch {
// No cooldown and no disable, unlike the responses path: the
// verdict a completion draws from a failed refresh is earned by a
// request the user actually made. Discovery fires on every CLI
// invocation, so letting it disable an account would turn a
// background probe into a forced re-login.
lastErrorStatus = 401;
lastErrorMessage = "Codex token refresh failed; re-login required";
break; // rotate
}
}
lastErrorStatus = upstream.status;
lastErrorMessage = "Codex model discovery rejected upstream";
break; // rotate
}
// Every other status — including a 400 — is upstream's real answer to a
// well-formed request and is relayed unchanged. A 400 here means the
// query was not forwarded correctly, and hiding it behind a retry would
// bury the exact regression this route was added to fix.
const body = await upstream.text();
if (upstream.ok) {
registerCodexDiscoveredLimits(body);
}
const contentType = upstream.headers.get("content-type") ?? "application/json";
return new Response(body, {
status: upstream.status,
headers: { "content-type": contentType },
});
}
}
// A discovery failure must not look like a missing route, or the next
// person debugging it re-opens this same issue.
return buildCodexErrorResponse(lastErrorStatus, lastErrorMessage);
}
/**
* Route-creation-time-only check: the Codex-outbound-fallback dispatcher's
* `anthropic` target needs the in-process loopback (`loopbackPort` and/or
* `internalDispatch`) to reach the Anthropic pool, and `createAllRoutes` /
* `registerAllRoutes` (`src/lib/server/routes/index.ts`) has no option that
* threads either through — only the CLI proxy command
* (`cli/commands/proxy.ts`) passes them directly to this function today. A
* consumer that enables the feature with an `anthropic` target through the
* generic SDK route surface would otherwise fail every such request silently
* behind `attemptCodexOutboundFallback`'s "no loopback port configured"
* warning, one per request, with nothing at startup explaining why. This
* logs ONE clear warning here instead, at the point the routes are built,
* rather than adding a new public option (ruling: don't grow
* `CreateRoutesOptions` for this — document the limitation and warn).
* A `vertex`-only configuration never reads `loopbackPort`, so it is exempt.
*/
function warnIfAnthropicOutboundFallbackUnreachable(runtimeConfigProvider, loopbackPort, internalDispatch) {
if (!runtimeConfigProvider ||
loopbackPort !== undefined ||
internalDispatch) {
return;
}
let snapshot;
try {
snapshot = runtimeConfigProvider();
}
catch {
// Same fail-open contract as the request-time reader below: a provider
// that throws at creation time is not this check's problem to surface.
return;
}
if (!snapshot.codexOutboundFallbackEnabled) {
return;
}
const hasAnthropicTarget = (snapshot.codexOutboundFallbackTargets ?? []).some((target) => target.provider === "anthropic");
if (!hasAnthropicTarget) {
return;
}
logger.warn("[codex-outbound-fallback] enabled with an anthropic target, but createCodexProxyRoutes " +
"was called without loopbackPort or internalDispatch — the anthropic leg cannot dispatch " +
"and every such request will fail at attempt time. This SDK route surface " +
"(createAllRoutes/registerAllRoutes) does not thread the in-process loopback through; " +
"only the CLI proxy command wires it. See docs/features/codex-proxy-support.md.");
}
/**
* Create Codex proxy routes.
*
* @param basePath - Base path prefix (default "").
* @param runtimeConfigProvider - Optional runtime config snapshot reader.
* Gates the Codex-outbound fallback dispatcher at all four insertion
* points in `dispatch()`: omitting it (or a snapshot with the feature
* disabled / no targets configured) reproduces today's behavior exactly.
* @param loopbackPort - The in-process HTTP port the outbound-fallback leg's
* Anthropic-pool loopback request targets (mirrors `createOpenAIProxyRoutes`'s
* bridge). Required only for an `anthropic` target; the `vertex` target
* dispatches via `executeVertexAnthropicFallback` and never reads it.
* @param internalDispatch - Optional in-process fetch (`(request) =>
* app.fetch(request)`), so the outbound-fallback loopback never re-enters
* the public listener. Falls back to a real `fetch` against
* `loopbackPort` when omitted.
* @returns RouteGroup with the Codex backend Responses endpoint.
*/
export function createCodexProxyRoutes(basePath = "", runtimeConfigProvider, loopbackPort, internalDispatch) {
warnIfAnthropicOutboundFallbackUnreachable(runtimeConfigProvider, loopbackPort, internalDispatch);
return {
prefix: `${basePath}/backend-api/codex`,
routes: [
{
method: "POST",
path: `${basePath}/backend-api/codex/responses`,
description: "Codex ChatGPT-backend Responses API (account pool)",
handler: (ctx) => handleCodexResponsesRequest(ctx, runtimeConfigProvider, loopbackPort, internalDispatch),
},
{
method: "GET",
path: `${basePath}/backend-api/codex/models`,
description: "Codex model discovery, relayed upstream (account pool)",
handler: (ctx) => handleCodexModelsRequest(ctx),
},
],
};
}
export const __testHooks = {
loadCodexProxyAccounts,
orderCodexAccounts,
buildCodexUpstreamHeaders,
planCodexCooldown,
refreshCodexTokenOnce,
refreshCodexTokenOnceWithDependencies,
codexRefreshInFlightSize: () => codexRefreshInFlight.size,
setOutboundFallbackTimeoutMsForTests: (ms) => {
codexOutboundFallbackTimeoutOverrideMs = ms;
},
wrapCodexOutboundResponseStream,
};