@juspay/neurolink 12.7.2 → 12.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,7 @@ import { configureProxyKeepAliveDispatcher } from "../../proxy/proxyDispatcher.j
25
25
  import { ProxyRuntimeConfigStore } from "../../proxy/runtimeConfig.js";
26
26
  import { startProxyLogCleanupScheduler } from "../../proxy/logCleanupScheduler.js";
27
27
  import { anthropicAccountKeysEqual, createAccountAllowlist, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
28
+ import { resolveProxyStatusAccountIdentity } from "../../proxy/codexAccountUsage.js";
28
29
  import { beginProxyRequest, getProxyActivitySnapshot, trackProxyResponse, } from "../../proxy/proxyActivity.js";
29
30
  import { flushProxyLifecycleEvents, getProxyLifecycleLoggerSnapshot, hashProxyLifecycleSessionId, logProxyLifecycleEvent, } from "../../proxy/proxyLifecycle.js";
30
31
  import { describeInstallFailure, getGlobalInstallArgs, isTransientInstallFailure, resolveGlobalInstaller, validateInstalledVersion, } from "../../proxy/globalInstaller.js";
@@ -1688,6 +1689,7 @@ export async function createProxyStartApp(params) {
1688
1689
  const storedAccountKeys = new Set();
1689
1690
  const storedAccountExpirations = new Map();
1690
1691
  const disabledAccountKeys = new Set();
1692
+ const disabledProviderAccountKeys = new Set();
1691
1693
  let accountInventoryLoaded = false;
1692
1694
  try {
1693
1695
  const { tokenStore } = await import("../../auth/tokenStore.js");
@@ -1720,6 +1722,7 @@ export async function createProxyStartApp(params) {
1720
1722
  }
1721
1723
  for (const key of inventory.disabledKeys) {
1722
1724
  disabledAccountKeys.add(normalizeAnthropicAccountKey(key));
1725
+ disabledProviderAccountKeys.add(key);
1723
1726
  }
1724
1727
  }
1725
1728
  catch (err) {
@@ -1742,28 +1745,46 @@ export async function createProxyStartApp(params) {
1742
1745
  }));
1743
1746
  const activeUpdaterPid = supervisorState?.updaterPid ?? runtimeState?.updaterPid;
1744
1747
  const accountRows = Object.values(stats.accounts).map((account) => {
1745
- const normalizedKey = normalizeAnthropicAccountKey(account.label);
1746
- const isLegacyAccount = account.type === "oauth" && account.label === legacyAccountLabel;
1747
- const accountKey = storedAccountKeys.has(normalizedKey)
1748
- ? normalizedKey
1749
- : isLegacyAccount
1750
- ? LEGACY_ANTHROPIC_ACCOUNT_KEY
1751
- : account.label === "env"
1752
- ? ENV_ANTHROPIC_ACCOUNT_KEY
1753
- : normalizedKey;
1754
- const isStored = storedAccountKeys.has(accountKey) || isLegacyAccount;
1755
- const { allowed, expired, cooling } = deriveAccountAllowance(accountKey, now, activeAccountAllowlist, storedAccountExpirations, cooldowns);
1748
+ const identity = resolveProxyStatusAccountIdentity(account.label, account.type);
1749
+ const isAnthropicAccount = identity.provider === "anthropic";
1750
+ const isCodexAccount = identity.provider === "codex";
1751
+ const normalizedKey = identity.provider === "anthropic" ? identity.key : null;
1752
+ const isLegacyAccount = isAnthropicAccount && account.label === legacyAccountLabel;
1753
+ const accountKey = isAnthropicAccount
1754
+ ? storedAccountKeys.has(normalizedKey ?? "")
1755
+ ? normalizedKey
1756
+ : isLegacyAccount
1757
+ ? LEGACY_ANTHROPIC_ACCOUNT_KEY
1758
+ : account.label === "env"
1759
+ ? ENV_ANTHROPIC_ACCOUNT_KEY
1760
+ : normalizedKey
1761
+ : identity.key;
1762
+ const isStored = isAnthropicAccount &&
1763
+ accountKey !== null &&
1764
+ (storedAccountKeys.has(accountKey) || isLegacyAccount);
1765
+ const { allowed, expired, cooling } = isAnthropicAccount && accountKey !== null
1766
+ ? deriveAccountAllowance(accountKey, now, activeAccountAllowlist, storedAccountExpirations, cooldowns)
1767
+ : {
1768
+ allowed: true,
1769
+ expired: false,
1770
+ cooling: accountKey !== null &&
1771
+ (cooldowns[accountKey]?.coolingUntil ?? 0) > now,
1772
+ };
1773
+ const isDisabled = (isAnthropicAccount &&
1774
+ accountKey !== null &&
1775
+ disabledAccountKeys.has(accountKey)) ||
1776
+ (isCodexAccount &&
1777
+ accountKey !== null &&
1778
+ disabledProviderAccountKeys.has(accountKey));
1756
1779
  const accountStatus = account.type === "internal"
1757
1780
  ? "internal"
1758
- : disabledAccountKeys.has(accountKey)
1781
+ : isDisabled
1759
1782
  ? "disabled"
1760
- : expired
1783
+ : isAnthropicAccount && expired
1761
1784
  ? "expired"
1762
- : !allowed
1785
+ : isAnthropicAccount && !allowed
1763
1786
  ? "excluded"
1764
- : accountInventoryLoaded &&
1765
- account.type === "oauth" &&
1766
- !isStored
1787
+ : accountInventoryLoaded && isAnthropicAccount && !isStored
1767
1788
  ? "removed"
1768
1789
  : cooling
1769
1790
  ? "cooling"
@@ -1782,11 +1803,11 @@ export async function createProxyStartApp(params) {
1782
1803
  cooling,
1783
1804
  status: accountStatus,
1784
1805
  allowed: account.type === "internal" ? undefined : allowed,
1785
- expired: account.type === "oauth" ? expired : undefined,
1806
+ expired: isAnthropicAccount ? expired : undefined,
1786
1807
  };
1787
1808
  });
1788
1809
  const representedAccountKeys = new Set(accountRows
1789
- .filter((account) => account.type === "oauth")
1810
+ .filter((account) => account.type === "oauth" || account.type === "api_key")
1790
1811
  .map((account) => normalizeAnthropicAccountKey(account.label)));
1791
1812
  for (const accountKey of storedAccountKeys) {
1792
1813
  if (representedAccountKeys.has(accountKey)) {
@@ -6,8 +6,13 @@
6
6
  * map onto the shared AccountQuota session/weekly fields so the same routing,
7
7
  * cooldown, and display code works for both providers.
8
8
  */
9
+ import type { CodexProxyStatusAccountIdentity } from "../types/index.js";
9
10
  import type { AccountQuota, CodexRateLimits, CodexUsageFetchResult, ProxyPassthroughAccount } from "../types/index.js";
10
11
  export declare const CODEX_ACCOUNT_PREFIX = "codex:";
12
+ /** Keep an account label in the Codex namespace used by its token store. */
13
+ export declare function normalizeCodexAccountKey(value: string): string;
14
+ /** Resolve the provider-qualified key used to render a proxy status row. */
15
+ export declare function resolveProxyStatusAccountIdentity(label: string, type: string): CodexProxyStatusAccountIdentity;
11
16
  /** Enumerate Codex OAuth accounts from the token store for usage refresh. */
12
17
  export declare function listCodexAccountsForUsage(): Promise<ProxyPassthroughAccount[]>;
13
18
  /** Normalise a Codex rate-limit block into the shared AccountQuota shape. */
@@ -7,9 +7,27 @@
7
7
  * cooldown, and display code works for both providers.
8
8
  */
9
9
  import { tokenStore } from "../auth/tokenStore.js";
10
+ import { normalizeAnthropicAccountKey } from "./accountSelection.js";
10
11
  import { CODEX_ORIGINATOR, CODEX_USAGE_URL, CODEX_USER_AGENT, decodeCodexAccessToken, resolveCodexAccountId, } from "../auth/codexOAuth.js";
11
12
  import { logger } from "../utils/logger.js";
12
13
  export const CODEX_ACCOUNT_PREFIX = "codex:";
14
+ /** Keep an account label in the Codex namespace used by its token store. */
15
+ export function normalizeCodexAccountKey(value) {
16
+ const trimmed = value.trim();
17
+ return trimmed.startsWith(CODEX_ACCOUNT_PREFIX)
18
+ ? trimmed
19
+ : `${CODEX_ACCOUNT_PREFIX}${trimmed}`;
20
+ }
21
+ /** Resolve the provider-qualified key used to render a proxy status row. */
22
+ export function resolveProxyStatusAccountIdentity(label, type) {
23
+ if (type === "oauth" || type === "api_key") {
24
+ return { provider: "anthropic", key: normalizeAnthropicAccountKey(label) };
25
+ }
26
+ if (type === "codex-oauth") {
27
+ return { provider: "codex", key: normalizeCodexAccountKey(label) };
28
+ }
29
+ return { provider: "other", key: null };
30
+ }
13
31
  /** Enumerate Codex OAuth accounts from the token store for usage refresh. */
14
32
  export async function listCodexAccountsForUsage() {
15
33
  const keys = await tokenStore.listByPrefix(CODEX_ACCOUNT_PREFIX);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Anthropic Messages API fallback over the pooled Codex Responses transport.
3
+ *
4
+ * This module deliberately contains only wire-format conversion and buffered
5
+ * SSE parsing. Account selection, OAuth, cooldowns, and quota persistence stay
6
+ * in the native Codex proxy handler so fallback traffic follows the same pool
7
+ * rules as a native Codex request.
8
+ */
9
+ import type { ClaudeRequest, CodexFallbackResult, CodexResponsesRequest } from "../types/index.js";
10
+ export declare class CodexFallbackResponseError extends Error {
11
+ readonly status: number;
12
+ readonly responseBody: string;
13
+ constructor(status: number, responseBody: string);
14
+ }
15
+ /** Convert a Claude Messages request into the ChatGPT Codex Responses shape. */
16
+ export declare function convertClaudeRequestToCodex(body: ClaudeRequest, model: string): CodexResponsesRequest;
17
+ /**
18
+ * Parse a complete Codex Responses SSE stream before emitting Claude output.
19
+ *
20
+ * A missing terminal event, malformed JSON, terminal error, or empty response
21
+ * is rejected. That makes it safe for the caller to try the next configured
22
+ * fallback without ever replaying output already sent to a client.
23
+ */
24
+ export declare function parseCodexFallbackSSE(sse: string): CodexFallbackResult;
25
+ /** Consume and validate a native Codex response before producing Claude output. */
26
+ export declare function consumeCodexFallbackResponse(response: Response): Promise<CodexFallbackResult>;
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Anthropic Messages API fallback over the pooled Codex Responses transport.
3
+ *
4
+ * This module deliberately contains only wire-format conversion and buffered
5
+ * SSE parsing. Account selection, OAuth, cooldowns, and quota persistence stay
6
+ * in the native Codex proxy handler so fallback traffic follows the same pool
7
+ * rules as a native Codex request.
8
+ */
9
+ import { extractCodexUsage } from "./codexUsage.js";
10
+ export class CodexFallbackResponseError extends Error {
11
+ status;
12
+ responseBody;
13
+ constructor(status, responseBody) {
14
+ super(`Codex fallback request returned HTTP ${status}`);
15
+ this.name = "CodexFallbackResponseError";
16
+ this.status = status;
17
+ this.responseBody = responseBody;
18
+ }
19
+ }
20
+ function isRecord(value) {
21
+ return value !== null && typeof value === "object" && !Array.isArray(value);
22
+ }
23
+ function asNonEmptyString(value) {
24
+ return typeof value === "string" && value.length > 0 ? value : undefined;
25
+ }
26
+ function buildSystemInstructions(body) {
27
+ if (typeof body.system === "string") {
28
+ return body.system || undefined;
29
+ }
30
+ if (Array.isArray(body.system)) {
31
+ const text = body.system
32
+ .map((block) => (typeof block.text === "string" ? block.text : ""))
33
+ .filter(Boolean)
34
+ .join("\n\n");
35
+ return text || undefined;
36
+ }
37
+ return undefined;
38
+ }
39
+ function imageUrlForBlock(block) {
40
+ if (block.source.type === "url" && block.source.url) {
41
+ return block.source.url;
42
+ }
43
+ if (block.source.type === "base64" && block.source.data) {
44
+ return `data:${block.source.media_type ?? "image/png"};base64,${block.source.data}`;
45
+ }
46
+ return undefined;
47
+ }
48
+ function flattenClaudeContent(content) {
49
+ if (typeof content === "string") {
50
+ return content;
51
+ }
52
+ return content
53
+ .map((block) => {
54
+ switch (block.type) {
55
+ case "text":
56
+ return block.text;
57
+ case "thinking":
58
+ return block.thinking;
59
+ case "image":
60
+ return "[image attachment]";
61
+ case "tool_use":
62
+ return `[tool call ${block.name}] ${JSON.stringify(block.input ?? {})}`;
63
+ case "tool_result":
64
+ return flattenClaudeContent(block.content);
65
+ }
66
+ })
67
+ .join("\n");
68
+ }
69
+ function toCodexContentPart(block, role) {
70
+ const textType = role === "assistant" ? "output_text" : "input_text";
71
+ switch (block.type) {
72
+ case "text":
73
+ return { type: textType, text: block.text };
74
+ case "thinking":
75
+ return { type: textType, text: block.thinking };
76
+ case "image": {
77
+ const imageUrl = imageUrlForBlock(block);
78
+ if (role === "user" && imageUrl) {
79
+ return { type: "input_image", image_url: imageUrl };
80
+ }
81
+ return { type: textType, text: "[image attachment]" };
82
+ }
83
+ }
84
+ }
85
+ function convertClaudeMessage(role, content) {
86
+ if (typeof content === "string") {
87
+ return [
88
+ {
89
+ role,
90
+ content: [
91
+ {
92
+ type: role === "assistant" ? "output_text" : "input_text",
93
+ text: content,
94
+ },
95
+ ],
96
+ },
97
+ ];
98
+ }
99
+ const input = [];
100
+ let messageContent = [];
101
+ const flushMessage = () => {
102
+ if (messageContent.length === 0) {
103
+ return;
104
+ }
105
+ input.push({ role, content: messageContent });
106
+ messageContent = [];
107
+ };
108
+ for (const block of content) {
109
+ if (block.type === "tool_use") {
110
+ flushMessage();
111
+ input.push({
112
+ type: "function_call",
113
+ call_id: block.id,
114
+ name: block.name,
115
+ arguments: JSON.stringify(block.input ?? {}),
116
+ });
117
+ continue;
118
+ }
119
+ if (block.type === "tool_result") {
120
+ flushMessage();
121
+ input.push({
122
+ type: "function_call_output",
123
+ call_id: block.tool_use_id,
124
+ output: flattenClaudeContent(block.content),
125
+ });
126
+ continue;
127
+ }
128
+ messageContent.push(toCodexContentPart(block, role));
129
+ }
130
+ flushMessage();
131
+ return input;
132
+ }
133
+ /** Convert a Claude Messages request into the ChatGPT Codex Responses shape. */
134
+ export function convertClaudeRequestToCodex(body, model) {
135
+ const input = body.messages.flatMap((message) => convertClaudeMessage(message.role, message.content));
136
+ const request = {
137
+ model,
138
+ input,
139
+ stream: true,
140
+ // ChatGPT's backend rejects requests unless this is explicitly false.
141
+ store: false,
142
+ };
143
+ const instructions = buildSystemInstructions(body);
144
+ if (instructions) {
145
+ request.instructions = instructions;
146
+ }
147
+ if (body.tools && body.tools.length > 0) {
148
+ request.tools = body.tools.map((tool) => ({
149
+ type: "function",
150
+ name: tool.name,
151
+ ...(tool.description ? { description: tool.description } : {}),
152
+ parameters: tool.input_schema,
153
+ }));
154
+ }
155
+ if (body.tool_choice) {
156
+ switch (body.tool_choice.type) {
157
+ case "any":
158
+ request.tool_choice = "required";
159
+ break;
160
+ case "tool":
161
+ request.tool_choice = { type: "function", name: body.tool_choice.name };
162
+ break;
163
+ default:
164
+ request.tool_choice = body.tool_choice.type;
165
+ break;
166
+ }
167
+ }
168
+ // The ChatGPT Codex backend is not the public Responses API. In particular,
169
+ // it rejects `max_output_tokens`, and its model-owned sampling controls do
170
+ // not map safely from Anthropic's `temperature` or `top_p`. Omit all three
171
+ // so the backend uses its supported defaults instead of rejecting fallback
172
+ // traffic before it can be served.
173
+ return request;
174
+ }
175
+ function parseFunctionArguments(value) {
176
+ if (isRecord(value)) {
177
+ return value;
178
+ }
179
+ if (typeof value !== "string") {
180
+ throw new Error("Codex fallback function call is missing JSON arguments");
181
+ }
182
+ try {
183
+ const parsed = JSON.parse(value || "{}");
184
+ if (!isRecord(parsed)) {
185
+ throw new Error("not an object");
186
+ }
187
+ return parsed;
188
+ }
189
+ catch {
190
+ throw new Error("Codex fallback function call returned invalid JSON arguments");
191
+ }
192
+ }
193
+ function outputTextFromItem(value) {
194
+ if (!isRecord(value) ||
195
+ value.type !== "message" ||
196
+ !Array.isArray(value.content)) {
197
+ return "";
198
+ }
199
+ return value.content
200
+ .filter(isRecord)
201
+ .filter((part) => part.type === "output_text")
202
+ .map((part) => (typeof part.text === "string" ? part.text : ""))
203
+ .join("");
204
+ }
205
+ function addFunctionCall(item, toolCalls) {
206
+ if (item.type !== "function_call") {
207
+ return;
208
+ }
209
+ const callId = asNonEmptyString(item.call_id);
210
+ const name = asNonEmptyString(item.name);
211
+ if (!callId || !name) {
212
+ throw new Error("Codex fallback function call is missing an id or name");
213
+ }
214
+ toolCalls.set(callId, {
215
+ toolCallId: callId,
216
+ toolName: name,
217
+ args: parseFunctionArguments(item.arguments),
218
+ });
219
+ }
220
+ function parseSSEPayloads(sse) {
221
+ const frames = sse.replace(/\r\n?/g, "\n").split("\n\n");
222
+ const parsed = [];
223
+ for (const frame of frames) {
224
+ if (!frame.trim()) {
225
+ continue;
226
+ }
227
+ let event;
228
+ const data = [];
229
+ for (const line of frame.split("\n")) {
230
+ if (!line || line.startsWith(":")) {
231
+ continue;
232
+ }
233
+ if (line.startsWith("event:")) {
234
+ event = line.slice("event:".length).trim();
235
+ continue;
236
+ }
237
+ if (line.startsWith("data:")) {
238
+ data.push(line.slice("data:".length).trimStart());
239
+ continue;
240
+ }
241
+ if (line.startsWith("id:") || line.startsWith("retry:")) {
242
+ continue;
243
+ }
244
+ // SSE permits extension fields that this parser does not consume.
245
+ continue;
246
+ }
247
+ if (data.length === 0) {
248
+ continue;
249
+ }
250
+ const raw = data.join("\n").trim();
251
+ if (raw === "[DONE]") {
252
+ continue;
253
+ }
254
+ try {
255
+ const payload = JSON.parse(raw);
256
+ if (!isRecord(payload)) {
257
+ throw new Error("not an object");
258
+ }
259
+ parsed.push({ event, payload });
260
+ }
261
+ catch {
262
+ throw new Error("Codex fallback stream contains malformed JSON");
263
+ }
264
+ }
265
+ return parsed;
266
+ }
267
+ function responseStatus(payload) {
268
+ const response = payload.response;
269
+ return isRecord(response) ? asNonEmptyString(response.status) : undefined;
270
+ }
271
+ function outputTextFromResponse(payload) {
272
+ const response = payload.response;
273
+ if (!isRecord(response) || !Array.isArray(response.output)) {
274
+ return "";
275
+ }
276
+ return response.output.map(outputTextFromItem).join("");
277
+ }
278
+ /**
279
+ * Parse a complete Codex Responses SSE stream before emitting Claude output.
280
+ *
281
+ * A missing terminal event, malformed JSON, terminal error, or empty response
282
+ * is rejected. That makes it safe for the caller to try the next configured
283
+ * fallback without ever replaying output already sent to a client.
284
+ */
285
+ export function parseCodexFallbackSSE(sse) {
286
+ const payloads = parseSSEPayloads(sse);
287
+ const toolCalls = new Map();
288
+ let textFromDeltas = "";
289
+ let textFromCompletedItems = "";
290
+ let textFromResponse = "";
291
+ let usage;
292
+ let sawCompleted = false;
293
+ for (const { event, payload } of payloads) {
294
+ const type = asNonEmptyString(payload.type) ?? event;
295
+ if (!type) {
296
+ throw new Error("Codex fallback stream event is missing a type");
297
+ }
298
+ if (type === "error" ||
299
+ type === "response.failed" ||
300
+ type === "response.incomplete") {
301
+ throw new Error(`Codex fallback stream terminated with ${type}`);
302
+ }
303
+ if (type === "response.output_text.delta") {
304
+ if (typeof payload.delta !== "string") {
305
+ throw new Error("Codex fallback text delta is malformed");
306
+ }
307
+ textFromDeltas += payload.delta;
308
+ continue;
309
+ }
310
+ if (type === "response.output_item.done") {
311
+ if (!isRecord(payload.item)) {
312
+ throw new Error("Codex fallback output item is malformed");
313
+ }
314
+ addFunctionCall(payload.item, toolCalls);
315
+ textFromCompletedItems += outputTextFromItem(payload.item);
316
+ continue;
317
+ }
318
+ if (type !== "response.completed") {
319
+ continue;
320
+ }
321
+ if (sawCompleted) {
322
+ throw new Error("Codex fallback stream emitted more than one completion event");
323
+ }
324
+ sawCompleted = true;
325
+ const status = responseStatus(payload);
326
+ if (status !== "completed") {
327
+ throw new Error(`Codex fallback stream completed with unexpected status ${status ?? "unknown"}`);
328
+ }
329
+ const parsedUsage = extractCodexUsage(payload);
330
+ if (parsedUsage) {
331
+ usage = {
332
+ input: parsedUsage.inputTokens,
333
+ output: parsedUsage.outputTokens,
334
+ total: parsedUsage.inputTokens + parsedUsage.outputTokens,
335
+ cacheReadTokens: parsedUsage.cacheReadTokens,
336
+ cacheCreationTokens: parsedUsage.cacheCreationTokens,
337
+ };
338
+ }
339
+ textFromResponse = outputTextFromResponse(payload);
340
+ }
341
+ if (!sawCompleted) {
342
+ throw new Error("Codex fallback stream ended before response.completed");
343
+ }
344
+ const text = textFromDeltas || textFromCompletedItems || textFromResponse;
345
+ const resolvedToolCalls = [...toolCalls.values()];
346
+ if (!text && resolvedToolCalls.length === 0) {
347
+ throw new Error("Codex fallback returned no content or tool calls");
348
+ }
349
+ return {
350
+ text,
351
+ toolCalls: resolvedToolCalls,
352
+ ...(usage ? { usage } : {}),
353
+ finishReason: resolvedToolCalls.length > 0 ? "tool_use" : "end_turn",
354
+ };
355
+ }
356
+ /** Consume and validate a native Codex response before producing Claude output. */
357
+ export async function consumeCodexFallbackResponse(response) {
358
+ if (!response.ok) {
359
+ throw new CodexFallbackResponseError(response.status, await response.text().catch(() => ""));
360
+ }
361
+ if (!response.body) {
362
+ throw new Error("Codex fallback returned an empty stream");
363
+ }
364
+ const contentType = response.headers.get("content-type") ?? "";
365
+ if (!contentType.toLowerCase().includes("text/event-stream")) {
366
+ // Consume it before failing so the underlying connection can be reused.
367
+ await response.text().catch(() => "");
368
+ throw new Error("Codex fallback returned a non-SSE response");
369
+ }
370
+ return parseCodexFallbackSSE(await response.text());
371
+ }
@@ -12,7 +12,7 @@
12
12
  import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
13
13
  import { ProxyTracer } from "../../proxy/proxyTracer.js";
14
14
  import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
15
- import type { AccountAllowlist, AccountAdmissionLease, JsonObject, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
15
+ import type { AccountAllowlist, AccountAdmissionLease, JsonObject, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicInvalidRequestFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
16
16
  declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined): AccountAdmissionLease | undefined;
17
17
  declare function enqueueAccountAdmission(accountKey: string, capacity: number): QueuedAccountAdmission;
18
18
  declare function acquireAccountAdmission(accountKey: string, capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<AccountAdmissionLease | undefined>;
@@ -173,6 +173,18 @@ declare function trackUpstreamReadableStream(source: ReadableStream<Uint8Array>)
173
173
  stream: ReadableStream<Uint8Array>;
174
174
  outcome: Promise<StreamTerminalOutcome>;
175
175
  };
176
+ /**
177
+ * Remove what a borrower has no business seeing.
178
+ *
179
+ * `x-neurolink-account` carries the lender's account label, which for an OAuth
180
+ * account is their email address; the pool counters describe the shape of a
181
+ * pool that is not the borrower's. The borrower's own routing needs the quota
182
+ * and grant headers, and nothing else here.
183
+ *
184
+ * A no-op for the node's own traffic, where these headers are exactly the
185
+ * diagnostics the operator wants.
186
+ */
187
+ declare function redactHeadersForBorrower(headers: Record<string, string>): Record<string, string>;
176
188
  declare function executeClaudeFallbackTranslation(args: {
177
189
  ctx: ServerContext;
178
190
  body: ClaudeRequest;
@@ -203,6 +215,7 @@ declare function executeClaudeFallbackTranslation(args: {
203
215
  idleTimeoutMs?: number;
204
216
  }): Promise<unknown>;
205
217
  declare function executeClaudeFallbackWithRetry(args: Parameters<typeof executeClaudeFallbackTranslation>[0]): Promise<unknown>;
218
+ declare function getCodexFallbackInvalidRequestFailure(error: unknown): AnthropicInvalidRequestFailure | null;
206
219
  declare function buildClaudeAnthropicFailureResponse(args: {
207
220
  tracer?: ProxyTracer;
208
221
  requestStartTime: number;
@@ -329,6 +342,7 @@ declare function handleAnthropicNonOkResponse(args: {
329
342
  contentType?: string;
330
343
  } | null;
331
344
  entitlementFailure: AnthropicEntitlementFailure | null;
345
+ allowConfiguredModelFallback?: boolean;
332
346
  }): Promise<AnthropicNonOkResult>;
333
347
  /**
334
348
  * Detect Anthropic's anti-abuse / request-construction 429.
@@ -388,6 +402,11 @@ export declare function parseClaudeErrorBody(errBody: string): ParsedClaudeError
388
402
  * Detect malformed request errors that should not trigger account/provider failover.
389
403
  */
390
404
  export declare function isInvalidRequestError(status: number, errBody: string): boolean;
405
+ /**
406
+ * A 404 for a retired model can be served by an explicitly configured fallback;
407
+ * other 404s remain terminal so a bad endpoint or resource is never disguised.
408
+ */
409
+ declare function isAnthropicModelNotFound(status: number, errBody: string): boolean;
391
410
  /**
392
411
  * A subscription-specific beta rejection. Anthropic returns
393
412
  * `400 invalid_request_error` with a message like
@@ -492,7 +511,10 @@ export declare const __testHooks: {
492
511
  redactProviderErrorMessage: typeof redactProviderErrorMessage;
493
512
  isUpstreamOverload: typeof isUpstreamOverload;
494
513
  getOverloadRotationDelayMs: typeof getOverloadRotationDelayMs;
514
+ redactHeadersForBorrower: typeof redactHeadersForBorrower;
495
515
  shouldAttemptClaudeFallback: typeof shouldAttemptClaudeFallback;
516
+ isAnthropicModelNotFound: typeof isAnthropicModelNotFound;
517
+ getCodexFallbackInvalidRequestFailure: typeof getCodexFallbackInvalidRequestFailure;
496
518
  executeClaudeFallbackWithRetry: typeof executeClaudeFallbackWithRetry;
497
519
  buildClaudeAnthropicFailureResponse: typeof buildClaudeAnthropicFailureResponse;
498
520
  isAccountEntitlementError: typeof isAccountEntitlementError;