@juspay/neurolink 12.7.2 → 12.7.4

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.
@@ -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
+ }
@@ -63,6 +63,13 @@ export function trackProxyResponse(response, finishRequest, observer) {
63
63
  let observedBodyBytes = 0;
64
64
  let responseChunks = 0;
65
65
  let settled = false;
66
+ let sourceClosed = false;
67
+ // A framework can cancel its adapter after the upstream stream has already
68
+ // closed. Snapshot reader.closed before cancel() so that normal cleanup is
69
+ // not recorded as a client-aborted request.
70
+ void reader.closed.then(() => {
71
+ sourceClosed = true;
72
+ }, () => undefined);
66
73
  const settle = (outcome) => {
67
74
  if (settled) {
68
75
  return;
@@ -100,7 +107,11 @@ export function trackProxyResponse(response, finishRequest, observer) {
100
107
  }
101
108
  },
102
109
  async cancel(reason) {
103
- settle("client_cancelled");
110
+ // Read the state before cancelling: reader.cancel() itself rejects the
111
+ // closed promise for a genuinely active source, which is too late to
112
+ // distinguish it from a source that had already ended normally.
113
+ await Promise.resolve();
114
+ settle(sourceClosed ? "completed" : "client_cancelled");
104
115
  await withTimeout(reader.cancel(reason), PROXY_RESPONSE_CANCEL_TIMEOUT_MS, "Timed out cancelling the upstream proxy response");
105
116
  },
106
117
  });
@@ -40,7 +40,7 @@ export declare class ProxyUsageStatsStore {
40
40
  recordAttemptError(accountLabel: string, accountType: string, status: number, rateLimitKind?: "transient" | "quota"): void;
41
41
  recordFinalError(status: number, accountLabel?: string, accountType?: string, details?: ProxyTerminalErrorDetails): void;
42
42
  getStats(): ProxyStats;
43
- getAccountStats(label: string): AccountStats | undefined;
43
+ getAccountStats(label: string, type?: string): AccountStats | undefined;
44
44
  getTerminalErrors(): ProxyTerminalErrorJournal;
45
45
  getUsageSnapshot(): ProxyUsageStatsSnapshot;
46
46
  getPersistenceStatus(): ProxyStatsPersistenceStatus;
@@ -81,7 +81,7 @@ export declare function getStats(): ProxyStats;
81
81
  export declare function getUsageSnapshot(): ProxyUsageStatsSnapshot;
82
82
  export declare function getReconciledStats(): Promise<ProxyStats>;
83
83
  export declare function getReconciledUsageSnapshot(): Promise<ProxyUsageStatsSnapshot>;
84
- export declare function getAccountStats(label: string): AccountStats | undefined;
84
+ export declare function getAccountStats(label: string, type?: string): AccountStats | undefined;
85
85
  export declare function getTerminalErrors(): ProxyTerminalErrorJournal;
86
86
  export declare function getUsageStatsPersistenceStatus(): ProxyStatsPersistenceStatus;
87
87
  export declare function flushUsageStats(): Promise<void>;
@@ -8,6 +8,7 @@
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
10
10
  import { basename, dirname, join } from "node:path";
11
+ import { normalizeAnthropicAccountKey } from "./accountSelection.js";
11
12
  import { AsyncMutex } from "../utils/asyncMutex.js";
12
13
  import { redactUrlsInText, sanitizeForLog } from "../utils/logSanitize.js";
13
14
  import { writeJsonSnapshotAtomically } from "./snapshotPersistence.js";
@@ -63,6 +64,34 @@ function emptyTerminalErrorJournal(startedAt) {
63
64
  recent: [],
64
65
  };
65
66
  }
67
+ /**
68
+ * Statistics must never use an email/label as their primary map key. One
69
+ * person can authenticate both provider pools with the same email, and a
70
+ * bare key silently merged their attempts, limits, and failures into one row.
71
+ *
72
+ * Keep non-account pseudo rows (passthrough, peer, internal) untouched. The
73
+ * display label remains separate so the CLI stays readable while persisted
74
+ * counters retain a collision-proof identity.
75
+ */
76
+ function resolveStatsAccountIdentity(label, type) {
77
+ if (type === "codex-oauth") {
78
+ const key = label.startsWith("codex:") ? label : `codex:${label}`;
79
+ return {
80
+ key,
81
+ label: label.startsWith("codex:") ? label.slice("codex:".length) : label,
82
+ };
83
+ }
84
+ if (type === "oauth" || type === "api_key") {
85
+ const key = normalizeAnthropicAccountKey(label);
86
+ return {
87
+ key,
88
+ label: label.startsWith("anthropic:")
89
+ ? label.slice("anthropic:".length)
90
+ : label,
91
+ };
92
+ }
93
+ return { key: label, label };
94
+ }
66
95
  function cloneTerminalErrorSummary(summary) {
67
96
  return { ...summary };
68
97
  }
@@ -152,6 +181,9 @@ function terminalErrorCategory(status, errorType) {
152
181
  }
153
182
  function createTerminalErrorSummary(args) {
154
183
  const { now, status, accountLabel, accountType, details } = args;
184
+ const accountIdentity = accountLabel && accountType
185
+ ? resolveStatsAccountIdentity(accountLabel, accountType)
186
+ : undefined;
155
187
  const errorType = clipTerminalErrorField(details?.errorType);
156
188
  const message = details?.message
157
189
  ? stripTerminalErrorControlCharacters(sanitizeForLog(redactUrlsInText(details.message), MAX_TERMINAL_ERROR_MESSAGE_LENGTH + MAX_TERMINAL_ERROR_FIELD_LENGTH))
@@ -170,6 +202,11 @@ function createTerminalErrorSummary(args) {
170
202
  ...(clipTerminalErrorField(accountLabel)
171
203
  ? { account: clipTerminalErrorField(accountLabel) }
172
204
  : {}),
205
+ ...(clipTerminalErrorField(details?.accountKey ?? accountIdentity?.key)
206
+ ? {
207
+ accountKey: clipTerminalErrorField(details?.accountKey ?? accountIdentity?.key),
208
+ }
209
+ : {}),
173
210
  ...(clipTerminalErrorField(accountType)
174
211
  ? { accountType: clipTerminalErrorField(accountType) }
175
212
  : {}),
@@ -201,7 +238,9 @@ function mergeAccountStats(left, right) {
201
238
  if (!left) {
202
239
  return cloneAccount(right);
203
240
  }
241
+ const key = right.key ?? left.key;
204
242
  return {
243
+ ...(key ? { key } : {}),
205
244
  label: right.label || left.label,
206
245
  type: right.type || left.type,
207
246
  attemptCount: left.attemptCount + right.attemptCount,
@@ -248,7 +287,9 @@ function validAccountStats(value) {
248
287
  return false;
249
288
  }
250
289
  const candidate = value;
251
- return (typeof candidate.label === "string" &&
290
+ return ((candidate.key === undefined ||
291
+ (typeof candidate.key === "string" && candidate.key.length > 0)) &&
292
+ typeof candidate.label === "string" &&
252
293
  typeof candidate.type === "string" &&
253
294
  finiteNonNegativeInteger(candidate.attemptCount) &&
254
295
  finiteNonNegativeInteger(candidate.attemptErrorCount) &&
@@ -282,7 +323,7 @@ function validStats(value) {
282
323
  typeof candidate.accounts !== "object") {
283
324
  return false;
284
325
  }
285
- return Object.entries(candidate.accounts).every(([label, account]) => validAccountStats(account) && label === account.label);
326
+ return Object.entries(candidate.accounts).every(([key, account]) => validAccountStats(account) && key === (account.key ?? account.label));
286
327
  }
287
328
  function validOptionalString(value, maxLength) {
288
329
  return (value === undefined ||
@@ -302,6 +343,7 @@ function validTerminalErrorSummary(value) {
302
343
  TERMINAL_ERROR_CATEGORIES.includes(candidate.category) &&
303
344
  validOptionalString(candidate.requestId, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
304
345
  validOptionalString(candidate.account, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
346
+ validOptionalString(candidate.accountKey, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
305
347
  validOptionalString(candidate.accountType, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
306
348
  validOptionalString(candidate.errorType, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
307
349
  validOptionalString(candidate.errorCode, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
@@ -615,8 +657,20 @@ export class ProxyUsageStatsStore {
615
657
  getStats() {
616
658
  return cloneStats(this.stats);
617
659
  }
618
- getAccountStats(label) {
619
- const account = this.stats.accounts[label];
660
+ getAccountStats(label, type) {
661
+ const direct = this.stats.accounts[label];
662
+ if (direct) {
663
+ return cloneAccount(direct);
664
+ }
665
+ const identity = type
666
+ ? resolveStatsAccountIdentity(label, type)
667
+ : undefined;
668
+ const account = identity
669
+ ? this.stats.accounts[identity.key]
670
+ : (() => {
671
+ const matches = Object.values(this.stats.accounts).filter((candidate) => candidate.label === label);
672
+ return matches.length === 1 ? matches[0] : undefined;
673
+ })();
620
674
  return account ? cloneAccount(account) : undefined;
621
675
  }
622
676
  getTerminalErrors() {
@@ -920,9 +974,11 @@ export class ProxyUsageStatsStore {
920
974
  }
921
975
  }
922
976
  ensureAccount(target, label, type) {
923
- if (!target.accounts[label]) {
924
- target.accounts[label] = {
925
- label,
977
+ const identity = resolveStatsAccountIdentity(label, type);
978
+ if (!target.accounts[identity.key]) {
979
+ target.accounts[identity.key] = {
980
+ key: identity.key,
981
+ label: identity.label,
926
982
  type,
927
983
  attemptCount: 0,
928
984
  attemptErrorCount: 0,
@@ -934,7 +990,7 @@ export class ProxyUsageStatsStore {
934
990
  lastAttemptAt: 0,
935
991
  };
936
992
  }
937
- return target.accounts[label];
993
+ return target.accounts[identity.key];
938
994
  }
939
995
  scheduleFlush() {
940
996
  if (!this.filePath || this.flushTimer) {
@@ -1131,8 +1187,8 @@ export async function getReconciledStats() {
1131
1187
  export async function getReconciledUsageSnapshot() {
1132
1188
  return defaultStore.reconcileUsageSnapshot();
1133
1189
  }
1134
- export function getAccountStats(label) {
1135
- return defaultStore.getAccountStats(label);
1190
+ export function getAccountStats(label, type) {
1191
+ return defaultStore.getAccountStats(label, type);
1136
1192
  }
1137
1193
  export function getTerminalErrors() {
1138
1194
  return defaultStore.getTerminalErrors();
@@ -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>;
@@ -65,7 +65,8 @@ declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: nu
65
65
  declare function reconcileCooldownFromQuota(state: RuntimeAccountState, quota: AccountQuota, now: number, policy?: ProxyOveragePolicy): ProxyQuotaCooldownUpdate;
66
66
  /**
67
67
  * Seed each account's runtime quota from the persisted snapshots in
68
- * ~/.neurolink/account-quotas.json (keyed by label). Runtime state is
68
+ * ~/.neurolink/account-quotas.json (keyed by provider-qualified account key).
69
+ * Runtime state is
69
70
  * in-memory only, so without this the quota-aware ordering is blind after a
70
71
  * proxy restart: all accounts tie, selection falls back to token-store
71
72
  * enumeration order, and the first account served becomes self-reinforcing
@@ -173,6 +174,18 @@ declare function trackUpstreamReadableStream(source: ReadableStream<Uint8Array>)
173
174
  stream: ReadableStream<Uint8Array>;
174
175
  outcome: Promise<StreamTerminalOutcome>;
175
176
  };
177
+ /**
178
+ * Remove what a borrower has no business seeing.
179
+ *
180
+ * `x-neurolink-account` carries the lender's account label, which for an OAuth
181
+ * account is their email address; the pool counters describe the shape of a
182
+ * pool that is not the borrower's. The borrower's own routing needs the quota
183
+ * and grant headers, and nothing else here.
184
+ *
185
+ * A no-op for the node's own traffic, where these headers are exactly the
186
+ * diagnostics the operator wants.
187
+ */
188
+ declare function redactHeadersForBorrower(headers: Record<string, string>): Record<string, string>;
176
189
  declare function executeClaudeFallbackTranslation(args: {
177
190
  ctx: ServerContext;
178
191
  body: ClaudeRequest;
@@ -203,6 +216,7 @@ declare function executeClaudeFallbackTranslation(args: {
203
216
  idleTimeoutMs?: number;
204
217
  }): Promise<unknown>;
205
218
  declare function executeClaudeFallbackWithRetry(args: Parameters<typeof executeClaudeFallbackTranslation>[0]): Promise<unknown>;
219
+ declare function getCodexFallbackInvalidRequestFailure(error: unknown): AnthropicInvalidRequestFailure | null;
206
220
  declare function buildClaudeAnthropicFailureResponse(args: {
207
221
  tracer?: ProxyTracer;
208
222
  requestStartTime: number;
@@ -329,6 +343,7 @@ declare function handleAnthropicNonOkResponse(args: {
329
343
  contentType?: string;
330
344
  } | null;
331
345
  entitlementFailure: AnthropicEntitlementFailure | null;
346
+ allowConfiguredModelFallback?: boolean;
332
347
  }): Promise<AnthropicNonOkResult>;
333
348
  /**
334
349
  * Detect Anthropic's anti-abuse / request-construction 429.
@@ -388,6 +403,11 @@ export declare function parseClaudeErrorBody(errBody: string): ParsedClaudeError
388
403
  * Detect malformed request errors that should not trigger account/provider failover.
389
404
  */
390
405
  export declare function isInvalidRequestError(status: number, errBody: string): boolean;
406
+ /**
407
+ * A 404 for a retired model can be served by an explicitly configured fallback;
408
+ * other 404s remain terminal so a bad endpoint or resource is never disguised.
409
+ */
410
+ declare function isAnthropicModelNotFound(status: number, errBody: string): boolean;
391
411
  /**
392
412
  * A subscription-specific beta rejection. Anthropic returns
393
413
  * `400 invalid_request_error` with a message like
@@ -492,7 +512,10 @@ export declare const __testHooks: {
492
512
  redactProviderErrorMessage: typeof redactProviderErrorMessage;
493
513
  isUpstreamOverload: typeof isUpstreamOverload;
494
514
  getOverloadRotationDelayMs: typeof getOverloadRotationDelayMs;
515
+ redactHeadersForBorrower: typeof redactHeadersForBorrower;
495
516
  shouldAttemptClaudeFallback: typeof shouldAttemptClaudeFallback;
517
+ isAnthropicModelNotFound: typeof isAnthropicModelNotFound;
518
+ getCodexFallbackInvalidRequestFailure: typeof getCodexFallbackInvalidRequestFailure;
496
519
  executeClaudeFallbackWithRetry: typeof executeClaudeFallbackWithRetry;
497
520
  buildClaudeAnthropicFailureResponse: typeof buildClaudeAnthropicFailureResponse;
498
521
  isAccountEntitlementError: typeof isAccountEntitlementError;