@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.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +407 -399
- package/dist/cli/commands/proxy.js +40 -19
- package/dist/localUsage/scanWindow.d.ts +34 -5
- package/dist/localUsage/scanWindow.js +34 -5
- package/dist/proxy/codexAccountUsage.d.ts +5 -0
- package/dist/proxy/codexAccountUsage.js +18 -0
- package/dist/proxy/codexFallback.d.ts +26 -0
- package/dist/proxy/codexFallback.js +371 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +23 -1
- package/dist/server/routes/claudeProxyRoutes.js +351 -96
- package/dist/server/routes/codexProxyRoutes.d.ts +3 -1
- package/dist/server/routes/codexProxyRoutes.js +1 -1
- package/dist/types/claudeProxy.d.ts +16 -0
- package/dist/types/claudeProxy.js +4 -0
- package/dist/types/codex.d.ts +62 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
1746
|
-
const
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
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
|
-
:
|
|
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:
|
|
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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
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
|
+
}
|