@juspay/neurolink 12.7.1 → 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)) {
@@ -16,14 +16,43 @@
16
16
  * Three fixes in three files each closed one door and left the others open.
17
17
  * The logic lives here once now, so the next door closes everywhere at once.
18
18
  *
19
- * Contract: `Infinity` is the ONLY value meaning all-history. Anything that is
20
- * not a usable finite window NaN, negative, or so large the arithmetic stops
21
- * being finite — collapses to a zero-length window, which is what a
22
- * nonsensical request should read as.
19
+ * Contract, stated as the two directions rather than as one rule, because the
20
+ * single-sentence version of it was wrong here for weeks:
21
+ *
22
+ * - `0`, negatives, `-Infinity` and `NaN` are NOT windows. They collapse to
23
+ * a zero-length one and read nothing. Two of the three historical defects
24
+ * above are here (`0` and `NaN`); `-Infinity` and negatives are the same
25
+ * class and were fixed with them.
26
+ * - `Infinity`, and any finite value whose window is longer than any
27
+ * history, mean ALL HISTORY. The THIRD historical defect — `MAX_VALUE` —
28
+ * lives in this bucket, not the one above: what was wrong about it was the
29
+ * mechanism (a `-Infinity` cutoff), never the outcome. A window of 1e308 days covers every
30
+ * transcript that could exist, so "everything" is the right answer to it
31
+ * and "nothing" is a wrong one.
32
+ *
33
+ * The prose here used to say the opposite of that second point — that a span
34
+ * too large to stay finite "collapses to a zero-length window". The code never
35
+ * did that, and the code was right. Anyone reconciling the two by changing the
36
+ * code re-creates a defect this suite has already seen once: asserting
37
+ * absurd-means-nothing produces a NON-MONOTONIC CLIFF, where 2.07e300 read
38
+ * everything, 2.09e300 read nothing, and `Infinity` read everything again.
39
+ * That was found by running the CLI — `usage local --since 999999999999`
40
+ * reported 518,576 turns while `--since 1e308` reported 39 — not by reading
41
+ * either the code or this comment.
42
+ *
43
+ * The invariant that actually holds, and the one worth testing, is
44
+ * MONOTONICITY: a wider window never reads less than a narrower one, across
45
+ * the whole range including the absurd end.
23
46
  */
24
47
  export declare const DEFAULT_SINCE_DAYS = 30;
25
48
  /**
26
49
  * @returns the epoch-ms cutoff a scan must not read past, or `undefined` for an
27
- * unbounded (all-history) scan — which only `Infinity` produces.
50
+ * unbounded (all-history) scan.
51
+ *
52
+ * `undefined` has TWO sources, not one, and this line used to name only the
53
+ * first: an explicit `Infinity`, and any finite request whose span overflows
54
+ * (`Number.MAX_VALUE`). They mean the same thing on purpose — see the contract
55
+ * above — but a reader who believes only `Infinity` reaches this branch will
56
+ * mis-handle the second.
28
57
  */
29
58
  export declare function resolveScanCutoffMs(sinceDays: number | undefined, defaultDays?: number): number | undefined;
@@ -16,16 +16,45 @@
16
16
  * Three fixes in three files each closed one door and left the others open.
17
17
  * The logic lives here once now, so the next door closes everywhere at once.
18
18
  *
19
- * Contract: `Infinity` is the ONLY value meaning all-history. Anything that is
20
- * not a usable finite window NaN, negative, or so large the arithmetic stops
21
- * being finite — collapses to a zero-length window, which is what a
22
- * nonsensical request should read as.
19
+ * Contract, stated as the two directions rather than as one rule, because the
20
+ * single-sentence version of it was wrong here for weeks:
21
+ *
22
+ * - `0`, negatives, `-Infinity` and `NaN` are NOT windows. They collapse to
23
+ * a zero-length one and read nothing. Two of the three historical defects
24
+ * above are here (`0` and `NaN`); `-Infinity` and negatives are the same
25
+ * class and were fixed with them.
26
+ * - `Infinity`, and any finite value whose window is longer than any
27
+ * history, mean ALL HISTORY. The THIRD historical defect — `MAX_VALUE` —
28
+ * lives in this bucket, not the one above: what was wrong about it was the
29
+ * mechanism (a `-Infinity` cutoff), never the outcome. A window of 1e308 days covers every
30
+ * transcript that could exist, so "everything" is the right answer to it
31
+ * and "nothing" is a wrong one.
32
+ *
33
+ * The prose here used to say the opposite of that second point — that a span
34
+ * too large to stay finite "collapses to a zero-length window". The code never
35
+ * did that, and the code was right. Anyone reconciling the two by changing the
36
+ * code re-creates a defect this suite has already seen once: asserting
37
+ * absurd-means-nothing produces a NON-MONOTONIC CLIFF, where 2.07e300 read
38
+ * everything, 2.09e300 read nothing, and `Infinity` read everything again.
39
+ * That was found by running the CLI — `usage local --since 999999999999`
40
+ * reported 518,576 turns while `--since 1e308` reported 39 — not by reading
41
+ * either the code or this comment.
42
+ *
43
+ * The invariant that actually holds, and the one worth testing, is
44
+ * MONOTONICITY: a wider window never reads less than a narrower one, across
45
+ * the whole range including the absurd end.
23
46
  */
24
47
  export const DEFAULT_SINCE_DAYS = 30;
25
48
  const MS_PER_DAY = 86_400_000;
26
49
  /**
27
50
  * @returns the epoch-ms cutoff a scan must not read past, or `undefined` for an
28
- * unbounded (all-history) scan — which only `Infinity` produces.
51
+ * unbounded (all-history) scan.
52
+ *
53
+ * `undefined` has TWO sources, not one, and this line used to name only the
54
+ * first: an explicit `Infinity`, and any finite request whose span overflows
55
+ * (`Number.MAX_VALUE`). They mean the same thing on purpose — see the contract
56
+ * above — but a reader who believes only `Infinity` reaches this branch will
57
+ * mis-handle the second.
29
58
  */
30
59
  export function resolveScanCutoffMs(sinceDays, defaultDays = DEFAULT_SINCE_DAYS) {
31
60
  const requested = sinceDays ?? defaultDays;
@@ -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
+ }