@jacobbd/relay-ai 0.7.6 → 0.9.0
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/README.md +26 -10
- package/dist/{chunk-HXGZ4CTV.js → chunk-Q2FTCICO.js} +19 -3
- package/dist/chunk-Q2FTCICO.js.map +1 -0
- package/dist/{chunk-GHSURQOK.js → chunk-R4AWEK7T.js} +922 -216
- package/dist/chunk-R4AWEK7T.js.map +1 -0
- package/dist/cli.js +1818 -434
- package/dist/cli.js.map +1 -1
- package/dist/core/index.js +718 -83
- package/dist/core/index.js.map +1 -1
- package/dist/{provider-templates-4H3C4DRL.js → provider-templates-XKNRKAQU.js} +2 -2
- package/dist/ui/public/app.js +270 -41
- package/dist/ui/public/index.html +32 -0
- package/dist/ui/public/provider-model-browser.js +18 -2
- package/dist/{ui-command-27X4WJKC.js → ui-command-FARF2BF4.js} +96 -15
- package/dist/ui-command-FARF2BF4.js.map +1 -0
- package/package.json +4 -3
- package/dist/chunk-GHSURQOK.js.map +0 -1
- package/dist/chunk-HXGZ4CTV.js.map +0 -1
- package/dist/ui-command-27X4WJKC.js.map +0 -1
- /package/dist/{provider-templates-4H3C4DRL.js.map → provider-templates-XKNRKAQU.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
BACKENDS,
|
|
5
5
|
CODEX_APP_AUTO_COMPACT_RATIO,
|
|
6
6
|
CODEX_APP_PROVIDER_ID,
|
|
7
|
+
CODEX_RESPONSES_LITE_VERSION,
|
|
8
|
+
CODEX_RESPONSES_LITE_WS_URL,
|
|
9
|
+
CODEX_RESPONSES_WEBSOCKETS_BETA,
|
|
10
|
+
CODEX_SUBAGENT_MODEL_CAP,
|
|
7
11
|
CONFLICTING_ENV_VARS,
|
|
8
12
|
GLOBAL_OPENCODE_KEYRING_ACCOUNT,
|
|
9
13
|
MAX_MODEL_CATALOG,
|
|
@@ -12,6 +16,7 @@ import {
|
|
|
12
16
|
VERSION,
|
|
13
17
|
VERTEX_ANTHROPIC_NPM,
|
|
14
18
|
addCustomEndpointProvider,
|
|
19
|
+
addFavorite,
|
|
15
20
|
addOpencodeCloudFromApiKey,
|
|
16
21
|
addProviderFromTemplate,
|
|
17
22
|
aliasModelId,
|
|
@@ -57,6 +62,7 @@ import {
|
|
|
57
62
|
fetchTemplateModels,
|
|
58
63
|
findBinaryOnPath,
|
|
59
64
|
findClaudeBinary,
|
|
65
|
+
findEmbeddedCodexBinary,
|
|
60
66
|
fmtCommand,
|
|
61
67
|
fmtCount,
|
|
62
68
|
fmtEnabledStar,
|
|
@@ -88,6 +94,7 @@ import {
|
|
|
88
94
|
injectRelayModels,
|
|
89
95
|
isClaudeAppRunning,
|
|
90
96
|
isCodexAppRunning,
|
|
97
|
+
isFavorite,
|
|
91
98
|
isFreeStatus,
|
|
92
99
|
isLikelyPlaceholderKey,
|
|
93
100
|
isOAuthImportProvider,
|
|
@@ -115,7 +122,9 @@ import {
|
|
|
115
122
|
parseCodexAppModelSlug,
|
|
116
123
|
parseDsmlToolCalls,
|
|
117
124
|
parseToolArguments,
|
|
125
|
+
preferredRelayCredentialAuthRef,
|
|
118
126
|
prepareClaudeTraceLog,
|
|
127
|
+
prepareProviderTraceLog,
|
|
119
128
|
printApiKeyPanel,
|
|
120
129
|
printCloudProviderPanel,
|
|
121
130
|
printDryRunPanel,
|
|
@@ -126,7 +135,9 @@ import {
|
|
|
126
135
|
printTraceLog,
|
|
127
136
|
printWelcomePanel,
|
|
128
137
|
providerAuthHelpText,
|
|
138
|
+
providerRefreshToken,
|
|
129
139
|
providerSelectOption,
|
|
140
|
+
providersForCodexSubagents,
|
|
130
141
|
providersForPicker,
|
|
131
142
|
providersForTarget,
|
|
132
143
|
quitClaudeAppGracefully,
|
|
@@ -135,13 +146,16 @@ import {
|
|
|
135
146
|
readFromCredentialStore,
|
|
136
147
|
readGlobalOpencodeCredential,
|
|
137
148
|
readOpencodeAuthFile,
|
|
149
|
+
readStoredProviderCredential,
|
|
138
150
|
recordLaunchSelection,
|
|
139
151
|
refreshAllProviderModels,
|
|
140
152
|
refreshModelsDevCacheAsync,
|
|
141
153
|
refreshProviderModels,
|
|
142
154
|
relayIntro,
|
|
143
155
|
relayOutro,
|
|
156
|
+
removeFavorite,
|
|
144
157
|
removeProviderFromRegistry,
|
|
158
|
+
renderMultiAgentV2Feature,
|
|
145
159
|
resetCodexBodyDumpLog,
|
|
146
160
|
resolveApiKey,
|
|
147
161
|
resolveContextWindow,
|
|
@@ -173,6 +187,7 @@ import {
|
|
|
173
187
|
startServer,
|
|
174
188
|
summarizeSdkRequestForTrace,
|
|
175
189
|
supportsClaudeTransparentMode,
|
|
190
|
+
supportsMultiAgentV2,
|
|
176
191
|
supportsNativeOAuth,
|
|
177
192
|
syntheticTemplate,
|
|
178
193
|
thinkingProviderOptions,
|
|
@@ -182,14 +197,15 @@ import {
|
|
|
182
197
|
validateCustomEndpointUrl,
|
|
183
198
|
writeSecureLogLine,
|
|
184
199
|
zenRegistryStub
|
|
185
|
-
} from "./chunk-
|
|
200
|
+
} from "./chunk-R4AWEK7T.js";
|
|
186
201
|
import {
|
|
187
202
|
filterTemplates,
|
|
203
|
+
getTemplateById,
|
|
188
204
|
init_provider_templates,
|
|
189
205
|
listAddableTemplates,
|
|
190
206
|
listSupportedTemplates,
|
|
191
207
|
listVisibleOAuthTemplates
|
|
192
|
-
} from "./chunk-
|
|
208
|
+
} from "./chunk-Q2FTCICO.js";
|
|
193
209
|
|
|
194
210
|
// src/cli.ts
|
|
195
211
|
import pc12 from "picocolors";
|
|
@@ -1102,19 +1118,6 @@ async function pickLocalModel(provider, conflicts, prefs) {
|
|
|
1102
1118
|
return selectedModel;
|
|
1103
1119
|
}
|
|
1104
1120
|
|
|
1105
|
-
// src/favorites.ts
|
|
1106
|
-
function isFavorite(list, fav) {
|
|
1107
|
-
return list.some((f) => f.providerId === fav.providerId && f.modelId === fav.modelId);
|
|
1108
|
-
}
|
|
1109
|
-
function addFavorite(list, fav, max = MAX_MODEL_CATALOG) {
|
|
1110
|
-
if (isFavorite(list, fav)) return { ok: false, reason: "duplicate" };
|
|
1111
|
-
if (list.length >= max) return { ok: false, reason: "cap" };
|
|
1112
|
-
return { ok: true, list: [...list, fav] };
|
|
1113
|
-
}
|
|
1114
|
-
function removeFavorite(list, fav) {
|
|
1115
|
-
return list.filter((f) => !(f.providerId === fav.providerId && f.modelId === fav.modelId));
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
1121
|
// src/favorites-picker.ts
|
|
1119
1122
|
import * as p4 from "@clack/prompts";
|
|
1120
1123
|
import pc3 from "picocolors";
|
|
@@ -1161,14 +1164,15 @@ function filterGlobalFavoriteIndex(entries, query, opts) {
|
|
|
1161
1164
|
return a.index - b.index;
|
|
1162
1165
|
}).map((result) => result.entry);
|
|
1163
1166
|
}
|
|
1164
|
-
function globalFavoriteSelectOption(entry, favorites) {
|
|
1167
|
+
function globalFavoriteSelectOption(entry, favorites, opts) {
|
|
1165
1168
|
const label = formatCodexModelLabel(entry.model);
|
|
1166
1169
|
const favorited = isFavorite(favorites, { providerId: entry.providerId, modelId: entry.model.id });
|
|
1167
1170
|
const providerTag = fmtProviderBracket(entry.providerId, entry.providerName, entry.model.isFree);
|
|
1171
|
+
const listLabel = opts?.listLabel ?? "favorites";
|
|
1168
1172
|
return {
|
|
1169
1173
|
value: globalFavoritePickKey(entry),
|
|
1170
1174
|
label: `${fmtModel(label, entry.model.id)} ${providerTag}`,
|
|
1171
|
-
hint: favorited ? pc3.dim(
|
|
1175
|
+
hint: favorited ? pc3.dim(`already in ${listLabel}`) : ""
|
|
1172
1176
|
};
|
|
1173
1177
|
}
|
|
1174
1178
|
function parseGlobalFavoritePickKey(key, index) {
|
|
@@ -1178,16 +1182,18 @@ async function pickGlobalFavoriteModel(providers, favorites, opts) {
|
|
|
1178
1182
|
const index = buildGlobalFavoriteIndex(providers);
|
|
1179
1183
|
if (index.length === 0) return null;
|
|
1180
1184
|
const freeOnly = opts?.freeOnly === true;
|
|
1185
|
+
const listLabel = opts?.listLabel ?? "favorites";
|
|
1186
|
+
const subagents = listLabel === "Codex Sub-agents";
|
|
1181
1187
|
while (true) {
|
|
1182
1188
|
const searchInput = await p4.text({
|
|
1183
|
-
message: freeOnly ? `Search free models (${filterGlobalFavoriteIndex(index, "", { freeOnly: true }).length} models):` :
|
|
1189
|
+
message: freeOnly ? `Search free models (${filterGlobalFavoriteIndex(index, "", { freeOnly: true }).length} models):` : `${subagents ? "Search all models" : "Search all providers"} (${index.length} models):`,
|
|
1184
1190
|
placeholder: "e.g. deepseek, claude, sonnet"
|
|
1185
1191
|
});
|
|
1186
1192
|
if (p4.isCancel(searchInput)) {
|
|
1187
1193
|
const fallback = await p4.select({
|
|
1188
|
-
message: "Add a favorite",
|
|
1194
|
+
message: subagents ? "Add a Codex Sub-agent model" : "Add a favorite",
|
|
1189
1195
|
options: [
|
|
1190
|
-
{ value: "back", label: pc3.cyan("\u2190 Back to favorites"), hint: "" },
|
|
1196
|
+
{ value: "back", label: pc3.cyan(subagents ? "\u2190 Back to Codex Sub-agents" : "\u2190 Back to favorites"), hint: "" },
|
|
1191
1197
|
{ value: ADD_BY_PROVIDER, label: pc3.cyan("Browse by provider \u2192"), hint: "Pick one provider first" }
|
|
1192
1198
|
]
|
|
1193
1199
|
});
|
|
@@ -1204,7 +1210,8 @@ async function pickGlobalFavoriteModel(providers, favorites, opts) {
|
|
|
1204
1210
|
matched.map((e) => ({ ...e, id: globalFavoritePickKey(e) })),
|
|
1205
1211
|
(e) => globalFavoriteSelectOption(
|
|
1206
1212
|
{ providerId: e.providerId, providerName: e.providerName, model: e.model },
|
|
1207
|
-
favorites
|
|
1213
|
+
favorites,
|
|
1214
|
+
{ listLabel }
|
|
1208
1215
|
),
|
|
1209
1216
|
matched.length === 1 ? "Match found" : `Select model (${matched.length} matches)`,
|
|
1210
1217
|
void 0,
|
|
@@ -1215,7 +1222,7 @@ async function pickGlobalFavoriteModel(providers, favorites, opts) {
|
|
|
1215
1222
|
const picked = parseGlobalFavoritePickKey(result.id, matched);
|
|
1216
1223
|
if (!picked) continue;
|
|
1217
1224
|
if (isFavorite(favorites, { providerId: picked.providerId, modelId: picked.model.id })) {
|
|
1218
|
-
p4.log.warn(`${picked.model.name || picked.model.id} (${picked.providerName}) is already in
|
|
1225
|
+
p4.log.warn(`${picked.model.name || picked.model.id} (${picked.providerName}) is already in ${listLabel}.`);
|
|
1219
1226
|
continue;
|
|
1220
1227
|
}
|
|
1221
1228
|
return picked;
|
|
@@ -1288,7 +1295,7 @@ ${pc4.bold("Subcommands:")}
|
|
|
1288
1295
|
(none) Provider hub wizard ${pc4.dim("[Phase 1.1]")}
|
|
1289
1296
|
add Add a provider (Groq, Mistral, Together AI, \u2026) ${pc4.dim("[Phase 1.1]")}
|
|
1290
1297
|
import Optional one-time import from OpenCode CLI ${pc4.dim("[Phase 1.0]")}
|
|
1291
|
-
auth Sign in with OAuth (GitHub Copilot, xAI, OpenAI)
|
|
1298
|
+
auth Sign in with OAuth (GitHub Copilot, xAI, OpenAI, ClinePass)
|
|
1292
1299
|
list Show configured providers ${pc4.dim("[Phase 1.0]")}
|
|
1293
1300
|
remove Remove a provider by id ${pc4.dim("[Phase 1.1]")}
|
|
1294
1301
|
refresh-models Update cached model lists ${pc4.dim("[Phase 1.2]")}`;
|
|
@@ -1510,6 +1517,69 @@ async function pickTemplateFromCatalog() {
|
|
|
1510
1517
|
if (template) return template;
|
|
1511
1518
|
}
|
|
1512
1519
|
}
|
|
1520
|
+
function hasApiAndOAuth(template) {
|
|
1521
|
+
const methods = template.authMethods ?? [template.authType];
|
|
1522
|
+
return methods.includes("api") && methods.includes("oauth");
|
|
1523
|
+
}
|
|
1524
|
+
function showProviderAddFailure(error, hint, fallback) {
|
|
1525
|
+
printPanel(pc4.red("Provider was not added"), [
|
|
1526
|
+
pc4.white(error ?? fallback),
|
|
1527
|
+
pc4.dim("No changes were saved."),
|
|
1528
|
+
...hint ? [pc4.white(hint)] : []
|
|
1529
|
+
]);
|
|
1530
|
+
}
|
|
1531
|
+
async function runDualAuthTemplateFlow(template, existing) {
|
|
1532
|
+
const method = await p5.select({
|
|
1533
|
+
message: existing ? `Change ${template.name} authentication` : `How would you like to connect to ${template.name}?`,
|
|
1534
|
+
options: [
|
|
1535
|
+
{
|
|
1536
|
+
value: "api",
|
|
1537
|
+
label: "Use an API key",
|
|
1538
|
+
hint: existing?.authType === "api" ? "Replace the current API key" : "Use your ClinePass API key"
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
value: "oauth",
|
|
1542
|
+
label: "Sign in with ClinePass",
|
|
1543
|
+
hint: "One-time device code; uses your ClinePass account"
|
|
1544
|
+
},
|
|
1545
|
+
{ value: "back", label: "Back", hint: "" }
|
|
1546
|
+
]
|
|
1547
|
+
});
|
|
1548
|
+
if (p5.isCancel(method) || method === "back") return 0;
|
|
1549
|
+
if (method === "oauth") {
|
|
1550
|
+
return runProvidersAuth(template.id);
|
|
1551
|
+
}
|
|
1552
|
+
if (template.signupUrl) {
|
|
1553
|
+
printPanel(fmtProvider(template.name), [
|
|
1554
|
+
`${pc4.white("Get an API key at:")} ${fmtUrl(template.signupUrl)}`
|
|
1555
|
+
]);
|
|
1556
|
+
}
|
|
1557
|
+
const apiKeyInput = await p5.password({
|
|
1558
|
+
message: `Paste your ${template.name} API key:`,
|
|
1559
|
+
validate: (value) => value.trim() ? void 0 : "Key cannot be empty"
|
|
1560
|
+
});
|
|
1561
|
+
if (p5.isCancel(apiKeyInput)) {
|
|
1562
|
+
p5.cancel("Cancelled.");
|
|
1563
|
+
return 0;
|
|
1564
|
+
}
|
|
1565
|
+
const apiKey = String(apiKeyInput).trim();
|
|
1566
|
+
if (!apiKey) {
|
|
1567
|
+
showProviderAddFailure("Key cannot be empty.", void 0, "Could not add provider.");
|
|
1568
|
+
return 1;
|
|
1569
|
+
}
|
|
1570
|
+
const spinner9 = p5.spinner();
|
|
1571
|
+
spinner9.start(`Testing connection to ${template.name}...`);
|
|
1572
|
+
const result = await addProviderFromTemplate(template, apiKey, {
|
|
1573
|
+
replaceExisting: Boolean(existing)
|
|
1574
|
+
});
|
|
1575
|
+
spinner9.stop("");
|
|
1576
|
+
if (!result.added) {
|
|
1577
|
+
showProviderAddFailure(result.error, result.hint, "Could not add provider.");
|
|
1578
|
+
return 1;
|
|
1579
|
+
}
|
|
1580
|
+
logConnected(template.name, result.modelCount ?? 0);
|
|
1581
|
+
return 0;
|
|
1582
|
+
}
|
|
1513
1583
|
async function runTemplateAddFlow() {
|
|
1514
1584
|
if (listAddableTemplates(loadRegistry().providers.map((p15) => p15.id)).length === 0) {
|
|
1515
1585
|
p5.log.info("All catalog providers are already configured.");
|
|
@@ -1517,6 +1587,9 @@ async function runTemplateAddFlow() {
|
|
|
1517
1587
|
}
|
|
1518
1588
|
const template = await pickTemplateFromCatalog();
|
|
1519
1589
|
if (!template) return 0;
|
|
1590
|
+
if (hasApiAndOAuth(template)) {
|
|
1591
|
+
return runDualAuthTemplateFlow(template);
|
|
1592
|
+
}
|
|
1520
1593
|
if (template.modelSource === "zen-go-api") {
|
|
1521
1594
|
const existingKey = await readGlobalOpencodeCredential();
|
|
1522
1595
|
let apiKey2 = existingKey;
|
|
@@ -1599,8 +1672,7 @@ async function runTemplateAddFlow() {
|
|
|
1599
1672
|
const result = await addProviderFromTemplate(template, apiKey, { baseUrl: baseUrlOverride });
|
|
1600
1673
|
spinner9.stop("");
|
|
1601
1674
|
if (!result.added) {
|
|
1602
|
-
|
|
1603
|
-
if (result.hint) p5.log.info(result.hint);
|
|
1675
|
+
showProviderAddFailure(result.error, result.hint, "Could not add provider.");
|
|
1604
1676
|
return 1;
|
|
1605
1677
|
}
|
|
1606
1678
|
logConnected(template.name, result.modelCount ?? 0);
|
|
@@ -1688,8 +1760,7 @@ async function runCustomEndpointAddFlow() {
|
|
|
1688
1760
|
});
|
|
1689
1761
|
spinner9.stop("");
|
|
1690
1762
|
if (!result.added) {
|
|
1691
|
-
|
|
1692
|
-
if (result.hint) p5.log.info(result.hint);
|
|
1763
|
+
showProviderAddFailure(result.error, result.hint, "Could not add custom provider.");
|
|
1693
1764
|
return 1;
|
|
1694
1765
|
}
|
|
1695
1766
|
logConnected(result.provider?.name ?? "Provider", result.modelCount ?? 0);
|
|
@@ -1792,6 +1863,8 @@ async function runProviderDetail(id) {
|
|
|
1792
1863
|
const modelCount = provider.modelsCache?.models.length ?? 0;
|
|
1793
1864
|
const authLabel = formatRegistryAuthLabel(provider);
|
|
1794
1865
|
printProviderDetailPanel(provider.name, modelCount, authLabel);
|
|
1866
|
+
const template = getTemplateById(provider.templateId) ?? getTemplateById(id);
|
|
1867
|
+
const hasDualAuth = template ? hasApiAndOAuth(template) : false;
|
|
1795
1868
|
const detailOptions = [];
|
|
1796
1869
|
if (modelCount > 0) {
|
|
1797
1870
|
detailOptions.push({
|
|
@@ -1805,13 +1878,26 @@ async function runProviderDetail(id) {
|
|
|
1805
1878
|
label: "Refresh model list",
|
|
1806
1879
|
hint: "Fetch latest models from the provider API"
|
|
1807
1880
|
});
|
|
1808
|
-
if (
|
|
1881
|
+
if (hasDualAuth && template) {
|
|
1882
|
+
detailOptions.push({
|
|
1883
|
+
value: "change-auth",
|
|
1884
|
+
label: "Change authentication (API/OAuth)",
|
|
1885
|
+
hint: "Switch between a ClinePass API key and account sign-in"
|
|
1886
|
+
});
|
|
1887
|
+
} else if (supportsNativeOAuth(id) || provider.authType === "oauth") {
|
|
1809
1888
|
detailOptions.push({
|
|
1810
1889
|
value: "auth",
|
|
1811
1890
|
label: "Sign in again (OAuth)",
|
|
1812
1891
|
hint: "Refresh OAuth tokens or switch accounts"
|
|
1813
1892
|
});
|
|
1814
1893
|
}
|
|
1894
|
+
if (provider.authType !== "oauth" && provider.authRef.startsWith("keyring:")) {
|
|
1895
|
+
detailOptions.push({
|
|
1896
|
+
value: "change-key",
|
|
1897
|
+
label: "Change API key",
|
|
1898
|
+
hint: "Test a new key and optionally replace the stored key"
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1815
1901
|
detailOptions.push(
|
|
1816
1902
|
{
|
|
1817
1903
|
value: "toggle",
|
|
@@ -1839,12 +1925,16 @@ async function runProviderDetail(id) {
|
|
|
1839
1925
|
return "back";
|
|
1840
1926
|
}
|
|
1841
1927
|
if (action === "refresh") {
|
|
1842
|
-
await runProvidersRefreshModels(id);
|
|
1843
|
-
|
|
1928
|
+
return await runProvidersRefreshModels(id) === 0 ? "back" : "failed";
|
|
1929
|
+
}
|
|
1930
|
+
if (action === "change-key") {
|
|
1931
|
+
return await runProviderApiKeyChange(provider, registry) === 0 ? "back" : "failed";
|
|
1932
|
+
}
|
|
1933
|
+
if (action === "change-auth" && template) {
|
|
1934
|
+
return await runDualAuthTemplateFlow(template, provider) === 0 ? "back" : "failed";
|
|
1844
1935
|
}
|
|
1845
1936
|
if (action === "auth") {
|
|
1846
|
-
await runProvidersAuth(id);
|
|
1847
|
-
return "back";
|
|
1937
|
+
return await runProvidersAuth(id) === 0 ? "back" : "failed";
|
|
1848
1938
|
}
|
|
1849
1939
|
if (action === "toggle") {
|
|
1850
1940
|
const result = toggleProviderEnabled(id);
|
|
@@ -1854,10 +1944,56 @@ async function runProviderDetail(id) {
|
|
|
1854
1944
|
return "back";
|
|
1855
1945
|
}
|
|
1856
1946
|
const code = await runProvidersRemove(id, true);
|
|
1857
|
-
return code === 0 ? "removed" : "
|
|
1947
|
+
return code === 0 ? "removed" : "failed";
|
|
1948
|
+
}
|
|
1949
|
+
async function runProviderApiKeyChange(provider, registry) {
|
|
1950
|
+
const entered = await p5.password({
|
|
1951
|
+
message: `Enter the new API key for ${provider.name}`,
|
|
1952
|
+
mask: "\u2022"
|
|
1953
|
+
});
|
|
1954
|
+
if (p5.isCancel(entered)) {
|
|
1955
|
+
p5.cancel("Cancelled.");
|
|
1956
|
+
return 0;
|
|
1957
|
+
}
|
|
1958
|
+
const key = String(entered).trim();
|
|
1959
|
+
if (!key) {
|
|
1960
|
+
p5.log.warn("API key cannot be empty.");
|
|
1961
|
+
return 1;
|
|
1962
|
+
}
|
|
1963
|
+
const targetAuthRef = preferredRelayCredentialAuthRef(provider.id, provider.authRef);
|
|
1964
|
+
let existing = await readStoredProviderCredential(targetAuthRef);
|
|
1965
|
+
if (!existing && targetAuthRef !== provider.authRef) {
|
|
1966
|
+
existing = await readStoredProviderCredential(provider.authRef);
|
|
1967
|
+
}
|
|
1968
|
+
if (existing && existing !== key) {
|
|
1969
|
+
const confirmed = await p5.confirm({
|
|
1970
|
+
message: "A different key is already stored. Replace it?",
|
|
1971
|
+
initialValue: false
|
|
1972
|
+
});
|
|
1973
|
+
if (p5.isCancel(confirmed) || !confirmed) {
|
|
1974
|
+
p5.log.info("Kept the existing stored key.");
|
|
1975
|
+
return 0;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
const spinner9 = p5.spinner();
|
|
1979
|
+
spinner9.start(`Testing ${provider.name} and refreshing models...`);
|
|
1980
|
+
const result = await refreshProviderModels(provider.id, key, registry);
|
|
1981
|
+
spinner9.stop("");
|
|
1982
|
+
if (!result.ok) {
|
|
1983
|
+
p5.log.error(`${provider.name}: ${result.reason ?? "The new key was rejected."}`);
|
|
1984
|
+
return 1;
|
|
1985
|
+
}
|
|
1986
|
+
const saved = await saveProviderCredential(targetAuthRef, key);
|
|
1987
|
+
if (!saved) {
|
|
1988
|
+
p5.log.error("The new key works, but the credential store was unavailable \u2014 key was not saved.");
|
|
1989
|
+
return 1;
|
|
1990
|
+
}
|
|
1991
|
+
p5.log.success(`${provider.name}: key updated and ${result.modelCount ?? 0} model${result.modelCount === 1 ? "" : "s"} available.`);
|
|
1992
|
+
return 0;
|
|
1858
1993
|
}
|
|
1859
1994
|
async function runProvidersHub() {
|
|
1860
1995
|
const hasOpencode = findOpencodeBinary() !== null;
|
|
1996
|
+
let lastOperationFailed = false;
|
|
1861
1997
|
while (true) {
|
|
1862
1998
|
const entries = await resolveProvidersForDisplay();
|
|
1863
1999
|
const options = [
|
|
@@ -1885,18 +2021,18 @@ async function runProvidersHub() {
|
|
|
1885
2021
|
options
|
|
1886
2022
|
});
|
|
1887
2023
|
if (p5.isCancel(choice) || choice === "done") {
|
|
1888
|
-
return 0;
|
|
2024
|
+
return lastOperationFailed ? 1 : 0;
|
|
1889
2025
|
}
|
|
1890
2026
|
if (choice === "add") {
|
|
1891
|
-
await runProvidersAdd();
|
|
2027
|
+
lastOperationFailed = await runProvidersAdd() !== 0;
|
|
1892
2028
|
continue;
|
|
1893
2029
|
}
|
|
1894
2030
|
if (choice === "import") {
|
|
1895
|
-
await runProvidersImport();
|
|
2031
|
+
lastOperationFailed = await runProvidersImport() !== 0;
|
|
1896
2032
|
continue;
|
|
1897
2033
|
}
|
|
1898
2034
|
if (choice === "refresh-all") {
|
|
1899
|
-
await runProvidersRefreshModels();
|
|
2035
|
+
lastOperationFailed = await runProvidersRefreshModels() !== 0;
|
|
1900
2036
|
continue;
|
|
1901
2037
|
}
|
|
1902
2038
|
if (choice === "auth-menu") {
|
|
@@ -1914,7 +2050,9 @@ async function runProvidersHub() {
|
|
|
1914
2050
|
hint: "device code"
|
|
1915
2051
|
}))
|
|
1916
2052
|
});
|
|
1917
|
-
if (!p5.isCancel(providerId))
|
|
2053
|
+
if (!p5.isCancel(providerId)) {
|
|
2054
|
+
lastOperationFailed = await runProvidersAuth(providerId) !== 0;
|
|
2055
|
+
}
|
|
1918
2056
|
continue;
|
|
1919
2057
|
}
|
|
1920
2058
|
if (typeof choice === "string" && choice.startsWith("cloud:")) {
|
|
@@ -1925,6 +2063,7 @@ async function runProvidersHub() {
|
|
|
1925
2063
|
if (typeof choice === "string" && choice.startsWith("provider:")) {
|
|
1926
2064
|
const id = choice.slice("provider:".length);
|
|
1927
2065
|
const outcome = await runProviderDetail(id);
|
|
2066
|
+
lastOperationFailed = outcome === "failed";
|
|
1928
2067
|
if (outcome === "removed") continue;
|
|
1929
2068
|
}
|
|
1930
2069
|
}
|
|
@@ -1958,10 +2097,13 @@ async function runProvidersCommand(args) {
|
|
|
1958
2097
|
// src/codex.ts
|
|
1959
2098
|
import pc7 from "picocolors";
|
|
1960
2099
|
import * as p8 from "@clack/prompts";
|
|
2100
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2101
|
+
import { join as join5 } from "path";
|
|
1961
2102
|
|
|
1962
2103
|
// src/codex-proxy.ts
|
|
1963
|
-
import { createHash } from "crypto";
|
|
2104
|
+
import { createHash as createHash2 } from "crypto";
|
|
1964
2105
|
import { createServer } from "http";
|
|
2106
|
+
import { WebSocket } from "ws";
|
|
1965
2107
|
|
|
1966
2108
|
// src/oauth/claude-code-identity.ts
|
|
1967
2109
|
function isClaudeCodeOAuthRoute(input) {
|
|
@@ -2067,8 +2209,8 @@ function extractDeveloperAndInstructions(items, instructions) {
|
|
|
2067
2209
|
const remaining = [];
|
|
2068
2210
|
for (const item of items) {
|
|
2069
2211
|
if ("role" in item && item.role === "developer") {
|
|
2070
|
-
const
|
|
2071
|
-
if (
|
|
2212
|
+
const text5 = messageText(item.content);
|
|
2213
|
+
if (text5.trim()) developerParts.push(text5.trim());
|
|
2072
2214
|
} else {
|
|
2073
2215
|
remaining.push(item);
|
|
2074
2216
|
}
|
|
@@ -2141,11 +2283,11 @@ function ensureUserFirst(messages) {
|
|
|
2141
2283
|
function reasoningSummaryText(item) {
|
|
2142
2284
|
return (item.summary ?? []).map((part) => part.type === "summary_text" ? part.text ?? "" : "").join("");
|
|
2143
2285
|
}
|
|
2144
|
-
function makeReasoningOutputItem(id,
|
|
2286
|
+
function makeReasoningOutputItem(id, text5) {
|
|
2145
2287
|
return {
|
|
2146
2288
|
id,
|
|
2147
2289
|
type: "reasoning",
|
|
2148
|
-
summary:
|
|
2290
|
+
summary: text5.trim() ? [{ type: "summary_text", text: text5 }] : []
|
|
2149
2291
|
};
|
|
2150
2292
|
}
|
|
2151
2293
|
function translateResponsesInput(input, instructions, npm, toolContext = createCodexToolContext()) {
|
|
@@ -2245,6 +2387,11 @@ function translateResponsesInput(input, instructions, npm, toolContext = createC
|
|
|
2245
2387
|
output: { type: "text", value: serializeToolResultContent(item.output) }
|
|
2246
2388
|
}]
|
|
2247
2389
|
});
|
|
2390
|
+
} else if (item.type === "agent_message") {
|
|
2391
|
+
const text5 = messageText(item.content);
|
|
2392
|
+
if (text5.trim()) {
|
|
2393
|
+
messages.push({ role: "user", content: [{ type: "text", text: text5 }] });
|
|
2394
|
+
}
|
|
2248
2395
|
} else if (item.type === "compaction" || item.type === "context_compaction") {
|
|
2249
2396
|
const summary = decodeCompactionContent(item.encrypted_content) ?? "";
|
|
2250
2397
|
if (summary.trim()) {
|
|
@@ -2253,8 +2400,8 @@ ${summary}` }] });
|
|
|
2253
2400
|
}
|
|
2254
2401
|
} else if ("role" in item) {
|
|
2255
2402
|
const role = item.role === "assistant" ? "assistant" : "user";
|
|
2256
|
-
const
|
|
2257
|
-
messages.push({ role, content: [{ type: "text", text:
|
|
2403
|
+
const text5 = messageText(item.content);
|
|
2404
|
+
messages.push({ role, content: [{ type: "text", text: text5 }] });
|
|
2258
2405
|
}
|
|
2259
2406
|
}
|
|
2260
2407
|
return {
|
|
@@ -2355,6 +2502,27 @@ function usageFromPart(part) {
|
|
|
2355
2502
|
const output = part.totalUsage?.outputTokens ?? 0;
|
|
2356
2503
|
return { input_tokens: input, output_tokens: output, total_tokens: input + output };
|
|
2357
2504
|
}
|
|
2505
|
+
var CODEX_SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set(["spawn_agent", "multi_agent_v1__spawn_agent"]);
|
|
2506
|
+
function isCodexSubagentToolName(toolName) {
|
|
2507
|
+
return CODEX_SUBAGENT_TOOL_NAMES.has(toolName) || toolName.endsWith("__spawn_agent");
|
|
2508
|
+
}
|
|
2509
|
+
function normalizeCodexSubagentArguments(toolName, argsStr) {
|
|
2510
|
+
if (!isCodexSubagentToolName(toolName)) return argsStr;
|
|
2511
|
+
const args = parseToolArguments(argsStr);
|
|
2512
|
+
for (const key of ["model", "reasoning_effort", "service_tier"]) {
|
|
2513
|
+
if (args[key] === "") delete args[key];
|
|
2514
|
+
}
|
|
2515
|
+
const items = args.items;
|
|
2516
|
+
const message = args.message;
|
|
2517
|
+
if (Array.isArray(items) && items.length > 0) {
|
|
2518
|
+
delete args.message;
|
|
2519
|
+
} else if (typeof message === "string" && message.trim()) {
|
|
2520
|
+
delete args.items;
|
|
2521
|
+
} else if (Array.isArray(items) && items.length === 0) {
|
|
2522
|
+
delete args.items;
|
|
2523
|
+
}
|
|
2524
|
+
return JSON.stringify(args);
|
|
2525
|
+
}
|
|
2358
2526
|
function resolveOutputKind(flatName, ctx) {
|
|
2359
2527
|
if (!ctx) return { kind: "plain" };
|
|
2360
2528
|
if (flatName === TOOL_SEARCH_NAME) return { kind: "tool_search" };
|
|
@@ -2364,6 +2532,7 @@ function resolveOutputKind(flatName, ctx) {
|
|
|
2364
2532
|
return { kind: "plain" };
|
|
2365
2533
|
}
|
|
2366
2534
|
function buildFinalToolItem(kind, flatName, callId, itemId, argsStr) {
|
|
2535
|
+
argsStr = normalizeCodexSubagentArguments(flatName, argsStr);
|
|
2367
2536
|
switch (kind.kind) {
|
|
2368
2537
|
case "namespace":
|
|
2369
2538
|
return { type: "function_call", id: itemId, call_id: callId, namespace: kind.namespace, name: kind.name, arguments: argsStr, status: "completed" };
|
|
@@ -2735,13 +2904,14 @@ async function writeResponsesStream(fullStream, modelId, write, onDone, onProgre
|
|
|
2735
2904
|
outputItems.unshift(reasoningItem);
|
|
2736
2905
|
}
|
|
2737
2906
|
for (const tool3 of toolStates) {
|
|
2907
|
+
const normalizedArgs = normalizeCodexSubagentArguments(tool3.name, tool3.args);
|
|
2738
2908
|
emit("response.function_call_arguments.done", {
|
|
2739
2909
|
type: "response.function_call_arguments.done",
|
|
2740
2910
|
item_id: tool3.itemId,
|
|
2741
2911
|
output_index: tool3.outputIndex,
|
|
2742
|
-
arguments:
|
|
2912
|
+
arguments: normalizedArgs
|
|
2743
2913
|
});
|
|
2744
|
-
const fcItem = buildFinalToolItem(resolveOutputKind(tool3.name, options?.toolContext), tool3.name, tool3.callId, tool3.itemId,
|
|
2914
|
+
const fcItem = buildFinalToolItem(resolveOutputKind(tool3.name, options?.toolContext), tool3.name, tool3.callId, tool3.itemId, normalizedArgs);
|
|
2745
2915
|
emit("response.output_item.done", {
|
|
2746
2916
|
type: "response.output_item.done",
|
|
2747
2917
|
output_index: tool3.outputIndex,
|
|
@@ -3024,6 +3194,412 @@ function responsesRateLimitBody(modelId, message) {
|
|
|
3024
3194
|
};
|
|
3025
3195
|
}
|
|
3026
3196
|
|
|
3197
|
+
// src/codex/routing.ts
|
|
3198
|
+
import { randomBytes } from "crypto";
|
|
3199
|
+
function classifyCodexDispatch(modelId, relayRoutes, nativeModelIds) {
|
|
3200
|
+
if (nativeModelIds.has(modelId)) return { kind: "native", modelId };
|
|
3201
|
+
const route = relayRoutes.find((candidate) => candidate.modelId === modelId);
|
|
3202
|
+
if (route) return { kind: "relay", route };
|
|
3203
|
+
return { kind: "unknown", modelId };
|
|
3204
|
+
}
|
|
3205
|
+
function createMixedProxyCapability() {
|
|
3206
|
+
return randomBytes(32).toString("base64url");
|
|
3207
|
+
}
|
|
3208
|
+
function mixedProxyBaseUrl(port, capability) {
|
|
3209
|
+
return `http://127.0.0.1:${port}/_relay-codex/${capability}`;
|
|
3210
|
+
}
|
|
3211
|
+
function parseMixedProxyPath(pathname, capability) {
|
|
3212
|
+
const prefix = `/_relay-codex/${capability}`;
|
|
3213
|
+
if (!pathname.startsWith(prefix)) return null;
|
|
3214
|
+
const suffix = pathname.slice(prefix.length);
|
|
3215
|
+
if (suffix !== "/v1/models" && suffix !== "/v1/responses" && suffix !== "/health") return null;
|
|
3216
|
+
return { capability, suffix };
|
|
3217
|
+
}
|
|
3218
|
+
function codexCompatibleProviders(providers, agent = "codex") {
|
|
3219
|
+
return providersForTarget(providers, agent);
|
|
3220
|
+
}
|
|
3221
|
+
function resolveBaseURL(model, provider) {
|
|
3222
|
+
if (provider.id === "zen" || provider.id === "go") {
|
|
3223
|
+
const isAnthropic = model.modelFormat === "anthropic";
|
|
3224
|
+
const baseUrl = BACKENDS[provider.id].baseUrl;
|
|
3225
|
+
return isAnthropic ? baseUrl : `${baseUrl}/v1`;
|
|
3226
|
+
}
|
|
3227
|
+
return model.apiBaseUrl ?? model.completionsUrl?.replace(/\/chat\/completions$/, "") ?? model.baseUrl;
|
|
3228
|
+
}
|
|
3229
|
+
function resolveCodexRoute(provider, model, apiKey) {
|
|
3230
|
+
const upstreamModelId = model.upstreamModelId || model.id;
|
|
3231
|
+
const inferredNpm = model.modelFormat === "anthropic" ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible";
|
|
3232
|
+
const isZenGo = provider.id === "zen" || provider.id === "go";
|
|
3233
|
+
const base = {
|
|
3234
|
+
npm: isZenGo ? inferredNpm : model.npm ?? inferredNpm,
|
|
3235
|
+
baseURL: resolveBaseURL(model, provider),
|
|
3236
|
+
upstreamModelId,
|
|
3237
|
+
apiKey,
|
|
3238
|
+
contextWindow: model.contextWindow,
|
|
3239
|
+
modelId: model.id,
|
|
3240
|
+
providerId: provider.id,
|
|
3241
|
+
authType: provider.authType,
|
|
3242
|
+
oauthAccountId: provider.oauthAccountId,
|
|
3243
|
+
providerData: provider.providerData,
|
|
3244
|
+
supportedParameters: model.supportedParameters,
|
|
3245
|
+
reasoning: model.reasoning,
|
|
3246
|
+
interleavedReasoningField: model.interleavedReasoningField,
|
|
3247
|
+
headers: provider.headers,
|
|
3248
|
+
refreshToken: providerRefreshToken(provider.id, provider.authType, provider.authRef)
|
|
3249
|
+
};
|
|
3250
|
+
if (model.modelFormat === "cloud-code") {
|
|
3251
|
+
return {
|
|
3252
|
+
tier: "cloud-code",
|
|
3253
|
+
npm: "@ai-sdk/anthropic",
|
|
3254
|
+
baseURL: "",
|
|
3255
|
+
upstreamModelId: model.upstreamModelId || model.id,
|
|
3256
|
+
apiKey,
|
|
3257
|
+
contextWindow: model.contextWindow,
|
|
3258
|
+
modelId: model.id,
|
|
3259
|
+
providerId: provider.id,
|
|
3260
|
+
authType: provider.authType,
|
|
3261
|
+
oauthAccountId: provider.oauthAccountId,
|
|
3262
|
+
providerData: provider.providerData,
|
|
3263
|
+
supportedParameters: model.supportedParameters,
|
|
3264
|
+
reasoning: model.reasoning,
|
|
3265
|
+
interleavedReasoningField: model.interleavedReasoningField,
|
|
3266
|
+
headers: provider.headers,
|
|
3267
|
+
refreshToken: providerRefreshToken(provider.id, provider.authType, provider.authRef)
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
if (model.npm === "@ai-sdk/openai" && provider.authType !== "oauth" && model.modelFormat === "openai") {
|
|
3271
|
+
return { tier: "direct", ...base };
|
|
3272
|
+
}
|
|
3273
|
+
return { tier: "proxy", ...base };
|
|
3274
|
+
}
|
|
3275
|
+
function routableModelsForProvider(provider, agent = "codex") {
|
|
3276
|
+
return routableModelsForTarget(provider, agent);
|
|
3277
|
+
}
|
|
3278
|
+
function codexProviderEnvKey(providerId) {
|
|
3279
|
+
const known = {
|
|
3280
|
+
openai: "OPENAI_API_KEY",
|
|
3281
|
+
xai: "XAI_API_KEY",
|
|
3282
|
+
"xai-oauth": "XAI_API_KEY",
|
|
3283
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
3284
|
+
google: "GEMINI_API_KEY"
|
|
3285
|
+
};
|
|
3286
|
+
return known[providerId] ?? `${providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
// src/codex/native-forward.ts
|
|
3290
|
+
var NATIVE_CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3291
|
+
var NATIVE_FORWARD_HEADERS = /* @__PURE__ */ new Set([
|
|
3292
|
+
"authorization",
|
|
3293
|
+
"chatgpt-account-id",
|
|
3294
|
+
"openai-beta",
|
|
3295
|
+
"originator",
|
|
3296
|
+
"session_id",
|
|
3297
|
+
"session-id",
|
|
3298
|
+
"thread_id",
|
|
3299
|
+
"thread-id",
|
|
3300
|
+
"turn_id",
|
|
3301
|
+
"turn-id",
|
|
3302
|
+
"user-agent",
|
|
3303
|
+
"version",
|
|
3304
|
+
"x-client-request-id",
|
|
3305
|
+
"x-codex-turn-metadata",
|
|
3306
|
+
"x-codex-turn-state",
|
|
3307
|
+
"x-codex-window-id",
|
|
3308
|
+
"x-codex-ws-stream-request-start-ms",
|
|
3309
|
+
"x-openai-internal-codex-responses-lite"
|
|
3310
|
+
]);
|
|
3311
|
+
var NATIVE_HEADER_NAMES = {
|
|
3312
|
+
"chatgpt-account-id": "ChatGPT-Account-Id",
|
|
3313
|
+
"openai-beta": "OpenAI-Beta"
|
|
3314
|
+
};
|
|
3315
|
+
function headerValue(headers, key) {
|
|
3316
|
+
const found = Object.entries(headers).find(([name]) => name.toLowerCase() === key);
|
|
3317
|
+
const value = found?.[1];
|
|
3318
|
+
return Array.isArray(value) ? value[0] : value;
|
|
3319
|
+
}
|
|
3320
|
+
function allowlistedNativeHeaders(inboundHeaders) {
|
|
3321
|
+
const out = {};
|
|
3322
|
+
for (const key of NATIVE_FORWARD_HEADERS) {
|
|
3323
|
+
const value = headerValue(inboundHeaders, key);
|
|
3324
|
+
if (!value) continue;
|
|
3325
|
+
out[NATIVE_HEADER_NAMES[key] ?? key] = value;
|
|
3326
|
+
}
|
|
3327
|
+
return out;
|
|
3328
|
+
}
|
|
3329
|
+
async function forwardNativeCodexHttp(options) {
|
|
3330
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
3331
|
+
const headers = allowlistedNativeHeaders(options.inboundHeaders);
|
|
3332
|
+
headers["content-type"] = headerValue(options.inboundHeaders, "content-type") ?? "application/json";
|
|
3333
|
+
return fetchImpl(options.nativeUrl ?? NATIVE_CODEX_RESPONSES_URL, {
|
|
3334
|
+
method: "POST",
|
|
3335
|
+
headers,
|
|
3336
|
+
body: options.body,
|
|
3337
|
+
signal: options.signal,
|
|
3338
|
+
redirect: "manual"
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
function nativeResponsesWebSocketOptions(options) {
|
|
3342
|
+
const headers = allowlistedNativeHeaders(options.headers);
|
|
3343
|
+
if (!headers["OpenAI-Beta"]) headers["OpenAI-Beta"] = CODEX_RESPONSES_WEBSOCKETS_BETA;
|
|
3344
|
+
if (!headers.version) headers.version = CODEX_RESPONSES_LITE_VERSION;
|
|
3345
|
+
if (!headers.originator) headers.originator = "codex_cli_rs";
|
|
3346
|
+
return {
|
|
3347
|
+
url: options.wsUrl ?? CODEX_RESPONSES_LITE_WS_URL,
|
|
3348
|
+
headers
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
// src/codex/collaboration-payload.ts
|
|
3353
|
+
import { createHash } from "crypto";
|
|
3354
|
+
var NATIVE_ENCRYPTED_TOKEN = /^gAAAAA[A-Za-z0-9_-]+={0,2}$/;
|
|
3355
|
+
var COLLABORATION_HEADER = /Message Type:\s*(?:NEW_TASK|MESSAGE|FOLLOWUP_TASK|FINAL_ANSWER)\b[\s\S]*\nPayload:\s*/i;
|
|
3356
|
+
var PAYLOAD_BOUNDARY = /^Payload:\s*$/m;
|
|
3357
|
+
var NATIVE_PAYLOAD_MAX_BYTES = 4 * 1024 * 1024;
|
|
3358
|
+
var NATIVE_PAYLOAD_CACHE_MAX_ENTRIES = 256;
|
|
3359
|
+
var NATIVE_PAYLOAD_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
3360
|
+
var AGENT_PAYLOAD_RELAY_TOOL = "relay_external_agent_payload";
|
|
3361
|
+
function itemContent(item) {
|
|
3362
|
+
return Array.isArray(item.content) ? item.content.filter((v) => v && typeof v === "object") : [];
|
|
3363
|
+
}
|
|
3364
|
+
function encryptedPart(item) {
|
|
3365
|
+
const part = itemContent(item).find((v) => v.type === "encrypted_content");
|
|
3366
|
+
return typeof part?.encrypted_content === "string" ? part.encrypted_content : void 0;
|
|
3367
|
+
}
|
|
3368
|
+
function visibleCollaborationText(item) {
|
|
3369
|
+
return itemContent(item).filter((v) => (v.type === "input_text" || v.type === "text") && typeof v.text === "string").map((v) => v.text).join("");
|
|
3370
|
+
}
|
|
3371
|
+
function envelopePayload(envelope) {
|
|
3372
|
+
if (!/^Message Type:\s*\S+/m.test(envelope)) return null;
|
|
3373
|
+
const boundary = envelope.match(PAYLOAD_BOUNDARY);
|
|
3374
|
+
if (!boundary || boundary.index === void 0) return null;
|
|
3375
|
+
const payload = envelope.slice(boundary.index + boundary[0].length).replace(/^\r?\n/, "");
|
|
3376
|
+
return payload.length > 0 ? payload : null;
|
|
3377
|
+
}
|
|
3378
|
+
function inspectCollaborationItem(item) {
|
|
3379
|
+
if (!item || typeof item !== "object") return { kind: "none" };
|
|
3380
|
+
const record = item;
|
|
3381
|
+
if (record.type === "compaction" || record.type === "context_compaction") return { kind: "none" };
|
|
3382
|
+
if (record.type !== "agent_message") return { kind: "none" };
|
|
3383
|
+
const visible = visibleCollaborationText(record);
|
|
3384
|
+
const encrypted = encryptedPart(record);
|
|
3385
|
+
if (encrypted === void 0) {
|
|
3386
|
+
const payload2 = COLLABORATION_HEADER.test(visible) ? envelopePayload(visible) : null;
|
|
3387
|
+
return payload2 !== null ? { kind: "relay-plaintext", plaintext: payload2 } : { kind: "malformed", reason: "agent_message has no encrypted_content part" };
|
|
3388
|
+
}
|
|
3389
|
+
if (NATIVE_ENCRYPTED_TOKEN.test(encrypted)) return { kind: "native-encrypted", ciphertext: encrypted };
|
|
3390
|
+
if (!COLLABORATION_HEADER.test(visible)) {
|
|
3391
|
+
return { kind: "malformed", reason: "unrecognized collaboration envelope" };
|
|
3392
|
+
}
|
|
3393
|
+
const payload = envelopePayload(encrypted);
|
|
3394
|
+
return { kind: "relay-plaintext", plaintext: payload ?? encrypted };
|
|
3395
|
+
}
|
|
3396
|
+
function normalizePlaintextCollaborationForExternal(input) {
|
|
3397
|
+
return input.map((item) => {
|
|
3398
|
+
const inspection = inspectCollaborationItem(item);
|
|
3399
|
+
if (inspection.kind !== "relay-plaintext") return item;
|
|
3400
|
+
if (encryptedPart(item) === void 0) return item;
|
|
3401
|
+
return replaceCollaborationPayload(item, inspection.plaintext);
|
|
3402
|
+
});
|
|
3403
|
+
}
|
|
3404
|
+
var COLLABORATION_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
3405
|
+
"collaboration",
|
|
3406
|
+
"spawn_agent",
|
|
3407
|
+
"wait_agent",
|
|
3408
|
+
"send_input",
|
|
3409
|
+
"close_agent",
|
|
3410
|
+
"list_agents"
|
|
3411
|
+
]);
|
|
3412
|
+
function isCollaborationTool(value) {
|
|
3413
|
+
if (!value || typeof value !== "object") return false;
|
|
3414
|
+
const tool3 = value;
|
|
3415
|
+
const name = typeof tool3.name === "string" ? tool3.name : "";
|
|
3416
|
+
if (tool3.type === "namespace") return name === "collaboration" || name === "multi_agent_v1";
|
|
3417
|
+
return COLLABORATION_TOOL_NAMES.has(name) || name.startsWith("collaboration__") || name.startsWith("multi_agent_v1__");
|
|
3418
|
+
}
|
|
3419
|
+
function stripCollaborationToolList(value) {
|
|
3420
|
+
if (!Array.isArray(value)) return value;
|
|
3421
|
+
return value.filter((tool3) => !isCollaborationTool(tool3)).map((tool3) => {
|
|
3422
|
+
if (!tool3 || typeof tool3 !== "object") return tool3;
|
|
3423
|
+
const record = tool3;
|
|
3424
|
+
if (!Array.isArray(record.tools)) return tool3;
|
|
3425
|
+
return { ...record, tools: stripCollaborationToolList(record.tools) };
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
function stripCodexCollaborationTools(body) {
|
|
3429
|
+
const stripped = { ...body };
|
|
3430
|
+
if (Array.isArray(body.tools)) stripped.tools = stripCollaborationToolList(body.tools);
|
|
3431
|
+
if (Array.isArray(body.input)) {
|
|
3432
|
+
stripped.input = body.input.map((item) => {
|
|
3433
|
+
if (!item || typeof item !== "object") return item;
|
|
3434
|
+
const record = item;
|
|
3435
|
+
if (record.type !== "additional_tools" || !Array.isArray(record.tools)) return item;
|
|
3436
|
+
return { ...record, tools: stripCollaborationToolList(record.tools) };
|
|
3437
|
+
});
|
|
3438
|
+
}
|
|
3439
|
+
return stripped;
|
|
3440
|
+
}
|
|
3441
|
+
function replaceCollaborationPayload(item, plaintext) {
|
|
3442
|
+
return {
|
|
3443
|
+
...item,
|
|
3444
|
+
content: [
|
|
3445
|
+
...item.content.filter((part) => part.type !== "encrypted_content"),
|
|
3446
|
+
{ type: "input_text", text: plaintext }
|
|
3447
|
+
]
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
function nativeHeaders(headers) {
|
|
3451
|
+
const out = {};
|
|
3452
|
+
for (const key of ["authorization", "chatgpt-account-id", "openai-beta", "originator", "session_id", "user-agent"]) {
|
|
3453
|
+
const value = headers[key] ?? headers[Object.keys(headers).find((k) => k.toLowerCase() === key) ?? ""];
|
|
3454
|
+
if (value) out[key === "chatgpt-account-id" ? "ChatGPT-Account-Id" : key] = value;
|
|
3455
|
+
}
|
|
3456
|
+
out["content-type"] = "application/json";
|
|
3457
|
+
out.Accept = "text/event-stream";
|
|
3458
|
+
return out;
|
|
3459
|
+
}
|
|
3460
|
+
function parsePayloadArguments(value) {
|
|
3461
|
+
try {
|
|
3462
|
+
const args = typeof value === "string" ? JSON.parse(value) : value;
|
|
3463
|
+
return typeof args?.payload === "string" ? args.payload : void 0;
|
|
3464
|
+
} catch {
|
|
3465
|
+
return void 0;
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
function parseFunctionCalls(value) {
|
|
3469
|
+
if (!value || typeof value !== "object") return [];
|
|
3470
|
+
const record = value;
|
|
3471
|
+
const direct = record.type === "function_call" ? [record] : [];
|
|
3472
|
+
const item = record.item && typeof record.item === "object" ? [record.item] : [];
|
|
3473
|
+
const output = Array.isArray(record.output) ? record.output : record.response && typeof record.response === "object" && Array.isArray(record.response.output) ? record.response.output : [];
|
|
3474
|
+
return [...direct, ...item, ...output].filter(
|
|
3475
|
+
(v) => v && typeof v === "object" && v.type === "function_call"
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
function parsePayloadResponse(text5) {
|
|
3479
|
+
const relayIds = /* @__PURE__ */ new Set();
|
|
3480
|
+
const completedPayloads = [];
|
|
3481
|
+
let argumentDeltas = "";
|
|
3482
|
+
for (const line of text5.split(/\r?\n/)) {
|
|
3483
|
+
if (!line.startsWith("data:")) continue;
|
|
3484
|
+
const data = line.slice(5).trim();
|
|
3485
|
+
if (!data || data === "[DONE]") continue;
|
|
3486
|
+
try {
|
|
3487
|
+
const event = JSON.parse(data);
|
|
3488
|
+
for (const call of parseFunctionCalls(event)) {
|
|
3489
|
+
if (call.name !== AGENT_PAYLOAD_RELAY_TOOL) continue;
|
|
3490
|
+
if (typeof call.id === "string") relayIds.add(call.id);
|
|
3491
|
+
if (typeof call.call_id === "string") relayIds.add(call.call_id);
|
|
3492
|
+
const payload = parsePayloadArguments(call.arguments);
|
|
3493
|
+
if (payload !== void 0) completedPayloads.push(payload);
|
|
3494
|
+
}
|
|
3495
|
+
const eventId = typeof event.item_id === "string" ? event.item_id : typeof event.call_id === "string" ? event.call_id : void 0;
|
|
3496
|
+
const related = relayIds.size === 0 || eventId !== void 0 && relayIds.has(eventId);
|
|
3497
|
+
if (related && event.type === "response.function_call_arguments.delta" && typeof event.delta === "string") {
|
|
3498
|
+
argumentDeltas += event.delta;
|
|
3499
|
+
}
|
|
3500
|
+
if (related && event.type === "response.function_call_arguments.done") {
|
|
3501
|
+
const payload = parsePayloadArguments(event.arguments);
|
|
3502
|
+
if (payload !== void 0) completedPayloads.push(payload);
|
|
3503
|
+
}
|
|
3504
|
+
} catch {
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
try {
|
|
3508
|
+
for (const call of parseFunctionCalls(JSON.parse(text5))) {
|
|
3509
|
+
if (call.name !== AGENT_PAYLOAD_RELAY_TOOL) continue;
|
|
3510
|
+
const payload = parsePayloadArguments(call.arguments);
|
|
3511
|
+
if (payload !== void 0) completedPayloads.push(payload);
|
|
3512
|
+
}
|
|
3513
|
+
} catch {
|
|
3514
|
+
}
|
|
3515
|
+
const accumulated = parsePayloadArguments(argumentDeltas);
|
|
3516
|
+
if (completedPayloads.length === 0 && accumulated !== void 0) completedPayloads.push(accumulated);
|
|
3517
|
+
const unique = [...new Set(completedPayloads)];
|
|
3518
|
+
if (unique.length !== 1 || unique[0].length === 0) {
|
|
3519
|
+
throw new Error("Native collaboration relay did not return exactly one task payload");
|
|
3520
|
+
}
|
|
3521
|
+
return unique[0];
|
|
3522
|
+
}
|
|
3523
|
+
function createNativePayloadRelay(options) {
|
|
3524
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
3525
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3526
|
+
let cacheBytes = 0;
|
|
3527
|
+
const removeCacheEntry = (key) => {
|
|
3528
|
+
const entry = cache.get(key);
|
|
3529
|
+
if (!entry) return;
|
|
3530
|
+
cache.delete(key);
|
|
3531
|
+
cacheBytes -= entry.bytes;
|
|
3532
|
+
};
|
|
3533
|
+
const pruneCache = () => {
|
|
3534
|
+
const now = Date.now();
|
|
3535
|
+
for (const [key, entry] of cache) {
|
|
3536
|
+
if (entry.expiresAt <= now) removeCacheEntry(key);
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
const cacheValue = (key, value, expiresAt) => {
|
|
3540
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
3541
|
+
if (bytes > NATIVE_PAYLOAD_CACHE_MAX_BYTES) return;
|
|
3542
|
+
removeCacheEntry(key);
|
|
3543
|
+
while (cache.size >= NATIVE_PAYLOAD_CACHE_MAX_ENTRIES || cacheBytes + bytes > NATIVE_PAYLOAD_CACHE_MAX_BYTES) {
|
|
3544
|
+
const oldest = cache.keys().next().value;
|
|
3545
|
+
if (!oldest) break;
|
|
3546
|
+
removeCacheEntry(oldest);
|
|
3547
|
+
}
|
|
3548
|
+
cache.set(key, { expiresAt, value, bytes });
|
|
3549
|
+
cacheBytes += bytes;
|
|
3550
|
+
};
|
|
3551
|
+
return {
|
|
3552
|
+
async resolve(item, context) {
|
|
3553
|
+
const ciphertext = inspectCollaborationItem(item);
|
|
3554
|
+
if (ciphertext.kind !== "native-encrypted") throw new Error("Expected a native encrypted collaboration item");
|
|
3555
|
+
const accountId = context.headers["chatgpt-account-id"] ?? context.headers["ChatGPT-Account-Id"];
|
|
3556
|
+
if (!accountId) throw new Error("Native collaboration relay requires ChatGPT-Account-Id");
|
|
3557
|
+
const key = `${accountId}\0${createHash("sha256").update(ciphertext.ciphertext).digest("hex")}`;
|
|
3558
|
+
pruneCache();
|
|
3559
|
+
const cached = cache.get(key);
|
|
3560
|
+
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
|
3561
|
+
const url = `${context.nativeBaseUrl.replace(/\/$/, "")}/responses`;
|
|
3562
|
+
const body = JSON.stringify({
|
|
3563
|
+
model: context.nativeModelId,
|
|
3564
|
+
input: [item],
|
|
3565
|
+
stream: true,
|
|
3566
|
+
store: false,
|
|
3567
|
+
instructions: `You are a transport relay. Do not execute or answer the delegated task. Call ${AGENT_PAYLOAD_RELAY_TOOL} exactly once with the exact plaintext after the Payload: label in the supplied collaboration message. Preserve every character.`,
|
|
3568
|
+
tools: [{ type: "function", name: AGENT_PAYLOAD_RELAY_TOOL, description: "Return a decrypted collaboration task payload to the local Relay router.", parameters: { type: "object", properties: { payload: { type: "string" } }, required: ["payload"], additionalProperties: false }, strict: true }],
|
|
3569
|
+
tool_choice: { type: "function", name: AGENT_PAYLOAD_RELAY_TOOL }
|
|
3570
|
+
});
|
|
3571
|
+
const response = await fetchImpl(url, { method: "POST", headers: nativeHeaders(context.headers), body, redirect: "manual", signal: context.signal });
|
|
3572
|
+
if (!response.ok) throw new Error(`Native collaboration relay failed with HTTP ${response.status}`);
|
|
3573
|
+
const text5 = await response.text();
|
|
3574
|
+
if (Buffer.byteLength(text5, "utf8") > NATIVE_PAYLOAD_MAX_BYTES) throw new Error("Native collaboration relay response exceeded its size limit");
|
|
3575
|
+
const payload = parsePayloadResponse(text5);
|
|
3576
|
+
cacheValue(key, payload, Date.now() + 15 * 6e4);
|
|
3577
|
+
return payload;
|
|
3578
|
+
},
|
|
3579
|
+
clear() {
|
|
3580
|
+
cache.clear();
|
|
3581
|
+
cacheBytes = 0;
|
|
3582
|
+
}
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
async function resolveRoutedCollaborationInput(input, context) {
|
|
3586
|
+
if (typeof input === "string") return input;
|
|
3587
|
+
const out = [];
|
|
3588
|
+
for (const item of input) {
|
|
3589
|
+
const inspection = inspectCollaborationItem(item);
|
|
3590
|
+
if (inspection.kind === "native-encrypted") {
|
|
3591
|
+
if (!context.relay) throw new Error("Codex encrypted the delegated sub-agent task, but Relay could not resolve it safely. The external provider was not contacted.");
|
|
3592
|
+
const payload = await context.relay.resolve(item, context.native);
|
|
3593
|
+
out.push(replaceCollaborationPayload(item, payload));
|
|
3594
|
+
} else if (inspection.kind === "malformed") {
|
|
3595
|
+
throw new Error(`Codex collaboration payload rejected: ${inspection.reason}`);
|
|
3596
|
+
} else {
|
|
3597
|
+
out.push(item);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
return normalizePlaintextCollaborationForExternal(out);
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3027
3603
|
// src/codex-proxy.ts
|
|
3028
3604
|
function captureCompletedResponse(sseText) {
|
|
3029
3605
|
if (!sseText.includes("response.completed")) return void 0;
|
|
@@ -3055,15 +3631,15 @@ function estimateCodexRequestChars(params) {
|
|
|
3055
3631
|
}
|
|
3056
3632
|
return chars;
|
|
3057
3633
|
}
|
|
3058
|
-
function clipTextForContext(
|
|
3059
|
-
if (
|
|
3634
|
+
function clipTextForContext(text5, maxChars) {
|
|
3635
|
+
if (text5.length <= maxChars) return text5;
|
|
3060
3636
|
const marker = `
|
|
3061
3637
|
|
|
3062
|
-
[... ${
|
|
3638
|
+
[... ${text5.length} chars clipped from oversized context item ...]
|
|
3063
3639
|
|
|
3064
3640
|
`;
|
|
3065
3641
|
const edge = Math.max(1, Math.floor((maxChars - marker.length) / 2));
|
|
3066
|
-
return `${
|
|
3642
|
+
return `${text5.slice(0, edge)}${marker}${text5.slice(-edge)}`;
|
|
3067
3643
|
}
|
|
3068
3644
|
function clipLargeTextParts(params, maxCharsPerPart) {
|
|
3069
3645
|
const messages = params.messages.map((msg) => {
|
|
@@ -3147,6 +3723,7 @@ function protectCodexCompactionParams(body, params, contextWindow) {
|
|
|
3147
3723
|
};
|
|
3148
3724
|
}
|
|
3149
3725
|
var PROXY_PLACEHOLDER_KEY = "proxy-local";
|
|
3726
|
+
var MAX_CODEX_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
3150
3727
|
function codexRouteLookupIds(requestedModel) {
|
|
3151
3728
|
const ids = routeLookupIds(requestedModel);
|
|
3152
3729
|
const bare = parseCodexAppModelSlug(requestedModel);
|
|
@@ -3175,6 +3752,27 @@ function findCodexProxyRoute(routes, requestedModel) {
|
|
|
3175
3752
|
}
|
|
3176
3753
|
return void 0;
|
|
3177
3754
|
}
|
|
3755
|
+
function requestHeaderValue(headers, name) {
|
|
3756
|
+
if (!headers) return void 0;
|
|
3757
|
+
const key = Object.keys(headers).find((candidate) => candidate.toLowerCase() === name);
|
|
3758
|
+
if (!key) return void 0;
|
|
3759
|
+
const value = headers[key];
|
|
3760
|
+
return Array.isArray(value) ? value[0] : value;
|
|
3761
|
+
}
|
|
3762
|
+
function isCodexSubagentRequest(body, headers) {
|
|
3763
|
+
const metadata = body.client_metadata;
|
|
3764
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
3765
|
+
const record = metadata;
|
|
3766
|
+
if (Object.prototype.hasOwnProperty.call(record, "x-openai-subagent")) return true;
|
|
3767
|
+
if (Object.prototype.hasOwnProperty.call(record, "x_openai_subagent")) return true;
|
|
3768
|
+
}
|
|
3769
|
+
return requestHeaderValue(headers, "x-openai-subagent") !== void 0;
|
|
3770
|
+
}
|
|
3771
|
+
function resolveCodexSubagentRoute(routes, configuredModelId, body, headers) {
|
|
3772
|
+
if (!isCodexSubagentRequest(body, headers)) return void 0;
|
|
3773
|
+
if (!configuredModelId) return void 0;
|
|
3774
|
+
return routes.find((route) => route.modelId === configuredModelId);
|
|
3775
|
+
}
|
|
3178
3776
|
function resolveModel(routes, models, requestedModel) {
|
|
3179
3777
|
const route = findCodexProxyRoute(routes, requestedModel);
|
|
3180
3778
|
if (!route) return void 0;
|
|
@@ -3182,10 +3780,30 @@ function resolveModel(routes, models, requestedModel) {
|
|
|
3182
3780
|
if (!languageModel) return void 0;
|
|
3183
3781
|
return { route, languageModel };
|
|
3184
3782
|
}
|
|
3783
|
+
async function prepareExternalCodexBody(body, context) {
|
|
3784
|
+
const externalBody = isCodexSubagentRequest(body, context.headers) ? stripCodexCollaborationTools(body) : body;
|
|
3785
|
+
if (!Array.isArray(externalBody.input)) return externalBody;
|
|
3786
|
+
const resolvedInput = await resolveRoutedCollaborationInput(
|
|
3787
|
+
externalBody.input,
|
|
3788
|
+
{
|
|
3789
|
+
relay: context.relay,
|
|
3790
|
+
native: {
|
|
3791
|
+
nativeBaseUrl: context.mixedNative?.nativeBaseUrl ?? "https://chatgpt.com/backend-api/codex",
|
|
3792
|
+
nativeModelId: context.mixedNative?.nativePayloadRelayModel ?? "gpt-5.5",
|
|
3793
|
+
headers: Object.fromEntries(Object.entries(context.headers).flatMap(([key, value]) => [
|
|
3794
|
+
[key, Array.isArray(value) ? value[0] : value ?? ""]
|
|
3795
|
+
]))
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
);
|
|
3799
|
+
return { ...externalBody, input: resolvedInput };
|
|
3800
|
+
}
|
|
3185
3801
|
async function startCodexProxy(routes, options = {}) {
|
|
3186
3802
|
const opts = typeof options === "boolean" ? { debug: options } : options;
|
|
3187
3803
|
const debug = opts.debug ?? false;
|
|
3188
3804
|
const requireAuth = opts.requireAuth ?? true;
|
|
3805
|
+
const mixedNative = opts.mixedNative;
|
|
3806
|
+
const nativePayloadRelay = mixedNative ? createNativePayloadRelay({}) : void 0;
|
|
3189
3807
|
silenceSdkWarnings();
|
|
3190
3808
|
const models = /* @__PURE__ */ new Map();
|
|
3191
3809
|
for (const route of routes) {
|
|
@@ -3199,7 +3817,11 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3199
3817
|
oauthAccountId: route.oauthAccountId,
|
|
3200
3818
|
providerData: route.providerData,
|
|
3201
3819
|
vertex: route.vertex,
|
|
3202
|
-
headers: route.headers
|
|
3820
|
+
headers: route.headers,
|
|
3821
|
+
refreshToken: route.refreshToken,
|
|
3822
|
+
onTokenRefreshed: (refreshed) => {
|
|
3823
|
+
route.apiKey = refreshed;
|
|
3824
|
+
}
|
|
3203
3825
|
}));
|
|
3204
3826
|
}
|
|
3205
3827
|
return new Promise((resolve2, reject2) => {
|
|
@@ -3212,6 +3834,17 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3212
3834
|
process.on("unhandledRejection", onRejection);
|
|
3213
3835
|
const server = createServer(async (req, res) => {
|
|
3214
3836
|
const url = req.url ?? "/";
|
|
3837
|
+
const parsedUrl = new URL(url, "http://127.0.0.1");
|
|
3838
|
+
const mixedPath = mixedNative ? parseMixedProxyPath(parsedUrl.pathname, mixedNative.capability) : null;
|
|
3839
|
+
if (mixedNative && !mixedPath) {
|
|
3840
|
+
sendJson(res, 404, { error: { message: "Not found", type: "invalid_request_error" } });
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
if (mixedNative && mixedPath && mixedPath.suffix === "/health" && req.method === "GET") {
|
|
3844
|
+
sendJson(res, 200, { ok: true });
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
3847
|
+
const effectivePath = mixedPath?.suffix ?? url;
|
|
3215
3848
|
if (debug) {
|
|
3216
3849
|
log14(`-> ${req.method} ${url} content-type=${req.headers["content-type"] ?? "(none)"} content-encoding=${req.headers["content-encoding"] ?? "(none)"} content-length=${req.headers["content-length"] ?? "(none)"}`);
|
|
3217
3850
|
}
|
|
@@ -3233,11 +3866,11 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3233
3866
|
return;
|
|
3234
3867
|
}
|
|
3235
3868
|
}
|
|
3236
|
-
if (req.method === "GET" &&
|
|
3869
|
+
if (req.method === "GET" && effectivePath === "/health") {
|
|
3237
3870
|
sendJson(res, 200, { ok: true });
|
|
3238
3871
|
return;
|
|
3239
3872
|
}
|
|
3240
|
-
if (req.method === "GET" &&
|
|
3873
|
+
if (req.method === "GET" && effectivePath === "/v1/models") {
|
|
3241
3874
|
const data = [];
|
|
3242
3875
|
const seenIds = /* @__PURE__ */ new Set();
|
|
3243
3876
|
const addModel = (id, providerId) => {
|
|
@@ -3257,14 +3890,21 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3257
3890
|
addModel(`${route.providerId}__${route.modelId}`, route.providerId);
|
|
3258
3891
|
}
|
|
3259
3892
|
}
|
|
3893
|
+
if (mixedNative) {
|
|
3894
|
+
for (const nativeModelId of mixedNative.nativeModelIds) addModel(nativeModelId, "openai");
|
|
3895
|
+
}
|
|
3260
3896
|
sendJson(res, 200, {
|
|
3261
3897
|
object: "list",
|
|
3262
3898
|
data
|
|
3263
3899
|
});
|
|
3264
3900
|
return;
|
|
3265
3901
|
}
|
|
3266
|
-
if (req.method === "GET" &&
|
|
3267
|
-
const id =
|
|
3902
|
+
if (req.method === "GET" && effectivePath.startsWith("/v1/models/")) {
|
|
3903
|
+
const id = effectivePath.slice("/v1/models/".length);
|
|
3904
|
+
if (mixedNative && mixedNative.nativeModelIds.has(id)) {
|
|
3905
|
+
sendJson(res, 200, { id, object: "model", created: Math.floor(Date.now() / 1e3), owned_by: "openai" });
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3268
3908
|
const route = findCodexProxyRoute(routes, id);
|
|
3269
3909
|
if (!route) {
|
|
3270
3910
|
sendJson(res, 404, { error: { message: `Model not found: ${id}`, type: "invalid_request_error" } });
|
|
@@ -3278,8 +3918,8 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3278
3918
|
});
|
|
3279
3919
|
return;
|
|
3280
3920
|
}
|
|
3281
|
-
if (req.method === "POST" &&
|
|
3282
|
-
if (requireAuth) {
|
|
3921
|
+
if (req.method === "POST" && effectivePath === "/v1/responses") {
|
|
3922
|
+
if (requireAuth && !mixedPath) {
|
|
3283
3923
|
const inboundKey = extractApiKey(req);
|
|
3284
3924
|
if (!inboundKey || inboundKey !== PROXY_PLACEHOLDER_KEY) {
|
|
3285
3925
|
sendJson(res, 401, { error: { message: "Unauthorized", type: "invalid_api_key" } });
|
|
@@ -3330,7 +3970,49 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3330
3970
|
}
|
|
3331
3971
|
}
|
|
3332
3972
|
const modelId = String(body.model ?? "");
|
|
3333
|
-
|
|
3973
|
+
const markedSubagent = Boolean(mixedNative && isCodexSubagentRequest(body, req.headers));
|
|
3974
|
+
const subagentRoute = mixedNative && markedSubagent ? resolveCodexSubagentRoute(routes, mixedNative.subagentRouteModelId, body, req.headers) : void 0;
|
|
3975
|
+
if (debug && markedSubagent) {
|
|
3976
|
+
log14(`subagent dispatch: requested=${modelId} route=${subagentRoute?.modelId ?? "(none)"}`);
|
|
3977
|
+
}
|
|
3978
|
+
if (mixedNative && markedSubagent && !subagentRoute) {
|
|
3979
|
+
sendJson(res, 503, {
|
|
3980
|
+
error: {
|
|
3981
|
+
message: "Codex marked this request as a Sub-agent, but no configured Codex Sub-agent route is available.",
|
|
3982
|
+
type: "service_unavailable"
|
|
3983
|
+
}
|
|
3984
|
+
});
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
if (mixedNative) {
|
|
3988
|
+
if (!markedSubagent) {
|
|
3989
|
+
const dispatch = classifyCodexDispatch(modelId, routes, mixedNative.nativeModelIds);
|
|
3990
|
+
if (dispatch.kind === "unknown") {
|
|
3991
|
+
sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
|
|
3992
|
+
return;
|
|
3993
|
+
}
|
|
3994
|
+
if (dispatch.kind === "native") {
|
|
3995
|
+
const controller = new AbortController();
|
|
3996
|
+
req.once("aborted", () => controller.abort());
|
|
3997
|
+
try {
|
|
3998
|
+
const nativeResponse = await forwardNativeCodexHttp({
|
|
3999
|
+
body: rawBody,
|
|
4000
|
+
inboundHeaders: req.headers,
|
|
4001
|
+
nativeUrl: mixedNative.nativeBaseUrl ? `${mixedNative.nativeBaseUrl.replace(/\/$/, "")}/responses` : NATIVE_CODEX_RESPONSES_URL,
|
|
4002
|
+
signal: controller.signal,
|
|
4003
|
+
fetchImpl: mixedNative.nativeFetchImpl
|
|
4004
|
+
});
|
|
4005
|
+
const contentType = nativeResponse.headers.get("content-type");
|
|
4006
|
+
res.writeHead(nativeResponse.status, contentType ? { "content-type": contentType } : void 0);
|
|
4007
|
+
res.end(Buffer.from(await nativeResponse.arrayBuffer()));
|
|
4008
|
+
} catch (err) {
|
|
4009
|
+
if (!res.writableEnded) sendJson(res, 502, { error: { message: "Native Codex request failed", type: "upstream_error" } });
|
|
4010
|
+
}
|
|
4011
|
+
return;
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
let resolved = subagentRoute ? resolveModel(routes, models, subagentRoute.modelId) : resolveModel(routes, models, modelId);
|
|
3334
4016
|
if (!resolved) {
|
|
3335
4017
|
const fallbackRoute = routes[0];
|
|
3336
4018
|
const fallbackLm = fallbackRoute ? models.get(fallbackRoute.modelId) : void 0;
|
|
@@ -3349,8 +4031,13 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3349
4031
|
}
|
|
3350
4032
|
const { route, languageModel } = resolved;
|
|
3351
4033
|
try {
|
|
4034
|
+
const routedBody = await prepareExternalCodexBody(body, {
|
|
4035
|
+
relay: nativePayloadRelay,
|
|
4036
|
+
mixedNative,
|
|
4037
|
+
headers: req.headers
|
|
4038
|
+
});
|
|
3352
4039
|
let params = applyClaudeCodeOAuthIdentity(route, translateResponsesRequest(
|
|
3353
|
-
|
|
4040
|
+
routedBody,
|
|
3354
4041
|
route.npm,
|
|
3355
4042
|
{
|
|
3356
4043
|
providerId: route.providerId,
|
|
@@ -3465,7 +4152,7 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3465
4152
|
sendJson(res, 404, { error: { message: "Not found", type: "invalid_request_error" } });
|
|
3466
4153
|
});
|
|
3467
4154
|
function wsAcceptKey(clientKey) {
|
|
3468
|
-
return
|
|
4155
|
+
return createHash2("sha1").update(clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64");
|
|
3469
4156
|
}
|
|
3470
4157
|
function wsDecodeFrame(buf) {
|
|
3471
4158
|
if (buf.length < 2) return null;
|
|
@@ -3480,9 +4167,12 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3480
4167
|
offset = 4;
|
|
3481
4168
|
} else if (payloadLen === 127) {
|
|
3482
4169
|
if (buf.length < 10) return null;
|
|
3483
|
-
|
|
4170
|
+
const declaredLength = buf.readBigUInt64BE(2);
|
|
4171
|
+
if (declaredLength > BigInt(MAX_CODEX_REQUEST_BYTES)) return { text: "", complete: true, opcode: -1 };
|
|
4172
|
+
payloadLen = Number(declaredLength);
|
|
3484
4173
|
offset = 10;
|
|
3485
4174
|
}
|
|
4175
|
+
if (payloadLen > MAX_CODEX_REQUEST_BYTES) return { text: "", complete: true, opcode: -1 };
|
|
3486
4176
|
const maskLen = masked ? 4 : 0;
|
|
3487
4177
|
if (buf.length < offset + maskLen + payloadLen) return null;
|
|
3488
4178
|
const mask = masked ? buf.slice(offset, offset + 4) : null;
|
|
@@ -3492,11 +4182,11 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3492
4182
|
payload[i] = buf[offset + i] ^ (mask ? mask[i % 4] : 0);
|
|
3493
4183
|
}
|
|
3494
4184
|
const opcode = b0 & 15;
|
|
3495
|
-
if (
|
|
3496
|
-
return { text: payload.toString("utf8"), complete: true };
|
|
4185
|
+
if (![1, 8, 9, 10].includes(opcode)) return { text: "", complete: true, opcode };
|
|
4186
|
+
return { text: payload.toString("utf8"), complete: true, opcode };
|
|
3497
4187
|
}
|
|
3498
|
-
function wsEncodeTextFrame(
|
|
3499
|
-
const payload = Buffer.from(
|
|
4188
|
+
function wsEncodeTextFrame(text5) {
|
|
4189
|
+
const payload = Buffer.from(text5, "utf8");
|
|
3500
4190
|
const len = payload.length;
|
|
3501
4191
|
let header;
|
|
3502
4192
|
if (len < 126) {
|
|
@@ -3514,13 +4204,33 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3514
4204
|
}
|
|
3515
4205
|
return Buffer.concat([header, payload]);
|
|
3516
4206
|
}
|
|
3517
|
-
function wsCloseFrame() {
|
|
3518
|
-
|
|
4207
|
+
function wsCloseFrame(code = 1e3) {
|
|
4208
|
+
const payload = Buffer.alloc(2);
|
|
4209
|
+
payload.writeUInt16BE(code, 0);
|
|
4210
|
+
return Buffer.concat([Buffer.from([136, 2]), payload]);
|
|
3519
4211
|
}
|
|
3520
4212
|
function wsPingFrame() {
|
|
3521
4213
|
return Buffer.from([137, 0]);
|
|
3522
4214
|
}
|
|
4215
|
+
function wsPongFrame(payload = "") {
|
|
4216
|
+
const bytes = Buffer.from(payload, "utf8");
|
|
4217
|
+
if (bytes.length > 125) return Buffer.from([138, 0]);
|
|
4218
|
+
return Buffer.concat([Buffer.from([138, bytes.length]), bytes]);
|
|
4219
|
+
}
|
|
3523
4220
|
server.on("upgrade", (req, socket, head) => {
|
|
4221
|
+
if (mixedNative) {
|
|
4222
|
+
const pathname = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
4223
|
+
const mixedPath = parseMixedProxyPath(pathname, mixedNative.capability);
|
|
4224
|
+
if (!mixedPath || mixedPath.suffix !== "/v1/responses") {
|
|
4225
|
+
socket.write("HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
|
4226
|
+
socket.destroy();
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
} else if (req.url !== "/v1/responses") {
|
|
4230
|
+
socket.write("HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
|
4231
|
+
socket.destroy();
|
|
4232
|
+
return;
|
|
4233
|
+
}
|
|
3524
4234
|
if (requireAuth) {
|
|
3525
4235
|
const inboundKey = extractApiKey(req);
|
|
3526
4236
|
if (!inboundKey || inboundKey !== PROXY_PLACEHOLDER_KEY) {
|
|
@@ -3545,10 +4255,12 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
|
|
|
3545
4255
|
);
|
|
3546
4256
|
let frameBuf = Buffer.alloc(0);
|
|
3547
4257
|
let handled = false;
|
|
4258
|
+
let nativeActive = false;
|
|
4259
|
+
let nativeUpstream;
|
|
3548
4260
|
let currentRequestModel = "";
|
|
3549
|
-
const closeSocket = () => {
|
|
4261
|
+
const closeSocket = (code = 1e3) => {
|
|
3550
4262
|
if (!socket.destroyed) {
|
|
3551
|
-
socket.write(wsCloseFrame());
|
|
4263
|
+
socket.write(wsCloseFrame(code));
|
|
3552
4264
|
socket.end();
|
|
3553
4265
|
}
|
|
3554
4266
|
};
|
|
@@ -3574,10 +4286,28 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
|
|
|
3574
4286
|
};
|
|
3575
4287
|
const onData = (chunk) => {
|
|
3576
4288
|
frameBuf = Buffer.concat([frameBuf, chunk]);
|
|
3577
|
-
if (handled) return;
|
|
4289
|
+
if (handled && !nativeActive) return;
|
|
3578
4290
|
const frame = wsDecodeFrame(frameBuf);
|
|
3579
4291
|
if (!frame) return;
|
|
3580
4292
|
frameBuf = Buffer.alloc(0);
|
|
4293
|
+
if (frame.opcode === 9) {
|
|
4294
|
+
socket.write(wsPongFrame(frame.text));
|
|
4295
|
+
return;
|
|
4296
|
+
}
|
|
4297
|
+
if (frame.opcode === 8) {
|
|
4298
|
+
closeSocket();
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
if (frame.opcode === -1) {
|
|
4302
|
+
socket.write(wsCloseFrame(1009));
|
|
4303
|
+
socket.end();
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
if (frame.opcode !== 1) {
|
|
4307
|
+
socket.write(wsCloseFrame(1003));
|
|
4308
|
+
socket.end();
|
|
4309
|
+
return;
|
|
4310
|
+
}
|
|
3581
4311
|
handled = true;
|
|
3582
4312
|
void (async () => {
|
|
3583
4313
|
let body;
|
|
@@ -3598,6 +4328,9 @@ data: ${JSON.stringify({ error: { message: "Invalid JSON", type: "invalid_reques
|
|
|
3598
4328
|
const tools = Array.isArray(body.tools) ? body.tools : [];
|
|
3599
4329
|
const toolNames = tools.map((t) => t && typeof t === "object" && "name" in t ? t.name : "?").join(",");
|
|
3600
4330
|
log14(`WS request: model=${String(body.model ?? "")} previous_response_id=${prevId ?? "(none)"} input_items=${inputItems} body_bytes=${frame.text.length} tools=[${toolNames || "none"}]`);
|
|
4331
|
+
const reasoning = body.reasoning && typeof body.reasoning === "object" ? Object.keys(body.reasoning).join(",") : typeof body.reasoning;
|
|
4332
|
+
const clientMetadata = body.client_metadata && typeof body.client_metadata === "object" ? Object.keys(body.client_metadata).join(",") : typeof body.client_metadata;
|
|
4333
|
+
log14(`WS request shape: stream=${String(body.stream)} store=${String(body.store)} generate=${String(body.generate)} parallel_tool_calls=${String(body.parallel_tool_calls)} reasoning_keys=[${reasoning || "none"}] include=${Array.isArray(body.include) ? body.include.join(",") : String(body.include)} client_metadata_keys=[${clientMetadata || "none"}]`);
|
|
3601
4334
|
appendCodexBodyDump({
|
|
3602
4335
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3603
4336
|
transport: "ws",
|
|
@@ -3610,7 +4343,145 @@ data: ${JSON.stringify({ error: { message: "Invalid JSON", type: "invalid_reques
|
|
|
3610
4343
|
}
|
|
3611
4344
|
const modelId = String(body.model ?? "");
|
|
3612
4345
|
currentRequestModel = modelId;
|
|
3613
|
-
|
|
4346
|
+
const markedSubagent = Boolean(mixedNative && isCodexSubagentRequest(body, req.headers));
|
|
4347
|
+
const subagentRoute = mixedNative && markedSubagent ? resolveCodexSubagentRoute(routes, mixedNative.subagentRouteModelId, body, req.headers) : void 0;
|
|
4348
|
+
if (debug && markedSubagent) {
|
|
4349
|
+
log14(`WS subagent dispatch: requested=${modelId} route=${subagentRoute?.modelId ?? "(none)"}`);
|
|
4350
|
+
}
|
|
4351
|
+
if (mixedNative && markedSubagent && !subagentRoute) {
|
|
4352
|
+
sendWsEvent(`event: error
|
|
4353
|
+
data: ${JSON.stringify({ error: {
|
|
4354
|
+
message: "Codex marked this request as a Sub-agent, but no configured Codex Sub-agent route is available.",
|
|
4355
|
+
type: "service_unavailable"
|
|
4356
|
+
} })}
|
|
4357
|
+
|
|
4358
|
+
`);
|
|
4359
|
+
closeSocket();
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
if (mixedNative) {
|
|
4363
|
+
if (!markedSubagent) {
|
|
4364
|
+
const dispatch = classifyCodexDispatch(modelId, routes, mixedNative.nativeModelIds);
|
|
4365
|
+
if (dispatch.kind === "unknown") {
|
|
4366
|
+
sendWsEvent(`event: error
|
|
4367
|
+
data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } })}
|
|
4368
|
+
|
|
4369
|
+
`);
|
|
4370
|
+
closeSocket();
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
if (dispatch.kind === "native") {
|
|
4374
|
+
if (nativeActive && nativeUpstream) {
|
|
4375
|
+
if (nativeUpstream.readyState === WebSocket.OPEN) {
|
|
4376
|
+
if (debug) log14(`WS native forwarding next turn: model=${modelId}`);
|
|
4377
|
+
nativeUpstream.send(JSON.stringify({ type: "response.create", ...body }));
|
|
4378
|
+
} else if (debug) {
|
|
4379
|
+
log14(`WS native cannot forward next turn: upstream_state=${nativeUpstream.readyState}`);
|
|
4380
|
+
}
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4383
|
+
const wsTarget = mixedNative.nativeBaseUrl ? `${mixedNative.nativeBaseUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:").replace(/\/$/, "")}/responses` : void 0;
|
|
4384
|
+
const target = nativeResponsesWebSocketOptions({ headers: req.headers, wsUrl: wsTarget });
|
|
4385
|
+
let upstream;
|
|
4386
|
+
let nativeOpened = false;
|
|
4387
|
+
let nativeCompleted = false;
|
|
4388
|
+
let nativeFrameCount = 0;
|
|
4389
|
+
let finished = false;
|
|
4390
|
+
let connectTimer;
|
|
4391
|
+
let firstFrameTimer;
|
|
4392
|
+
const clearTimers = () => {
|
|
4393
|
+
if (connectTimer) clearTimeout(connectTimer);
|
|
4394
|
+
if (firstFrameTimer) clearTimeout(firstFrameTimer);
|
|
4395
|
+
};
|
|
4396
|
+
const sendNativeError = (message) => {
|
|
4397
|
+
if (socket.destroyed) return;
|
|
4398
|
+
socket.write(wsEncodeTextFrame(JSON.stringify({
|
|
4399
|
+
type: "error",
|
|
4400
|
+
error: { type: "upstream_error", message }
|
|
4401
|
+
})));
|
|
4402
|
+
};
|
|
4403
|
+
const closeBoth = (message, closeCode = 1011) => {
|
|
4404
|
+
if (finished) return;
|
|
4405
|
+
finished = true;
|
|
4406
|
+
nativeActive = false;
|
|
4407
|
+
if (nativeUpstream === upstream) nativeUpstream = void 0;
|
|
4408
|
+
clearTimers();
|
|
4409
|
+
if (debug && message) {
|
|
4410
|
+
log14(`WS native upstream failed: model=${modelId} opened=${nativeOpened} frames=${nativeFrameCount} message=${message}`);
|
|
4411
|
+
}
|
|
4412
|
+
if (message && !nativeCompleted) sendNativeError(message);
|
|
4413
|
+
try {
|
|
4414
|
+
upstream?.close();
|
|
4415
|
+
} catch {
|
|
4416
|
+
}
|
|
4417
|
+
closeSocket(closeCode);
|
|
4418
|
+
};
|
|
4419
|
+
try {
|
|
4420
|
+
if (debug) {
|
|
4421
|
+
log14(`WS native connecting: model=${modelId} url=${target.url} headers=[${Object.keys(target.headers).join(",")}]`);
|
|
4422
|
+
}
|
|
4423
|
+
upstream = new WebSocket(target.url, { headers: target.headers });
|
|
4424
|
+
nativeUpstream = upstream;
|
|
4425
|
+
nativeActive = true;
|
|
4426
|
+
connectTimer = setTimeout(() => closeBoth("Native Codex WebSocket connection timed out"), 15e3);
|
|
4427
|
+
upstream.once("open", () => {
|
|
4428
|
+
nativeOpened = true;
|
|
4429
|
+
if (connectTimer) clearTimeout(connectTimer);
|
|
4430
|
+
if (debug) log14(`WS native upstream open: model=${modelId}`);
|
|
4431
|
+
upstream?.send(JSON.stringify({ type: "response.create", ...body }));
|
|
4432
|
+
firstFrameTimer = setTimeout(() => closeBoth("Native Codex WebSocket response timed out"), 6e4);
|
|
4433
|
+
});
|
|
4434
|
+
upstream.once("unexpected-response", (_request, response) => {
|
|
4435
|
+
if (debug) log14(`WS native upstream HTTP rejection: model=${modelId} status=${response.statusCode}`);
|
|
4436
|
+
response.resume();
|
|
4437
|
+
closeBoth(`Native Codex WebSocket rejected (${response.statusCode})`);
|
|
4438
|
+
});
|
|
4439
|
+
upstream.on("message", (data) => {
|
|
4440
|
+
if (socket.destroyed) return;
|
|
4441
|
+
nativeFrameCount += 1;
|
|
4442
|
+
if (firstFrameTimer) clearTimeout(firstFrameTimer);
|
|
4443
|
+
const text5 = Array.isArray(data) ? Buffer.concat(data).toString("utf8") : data.toString("utf8");
|
|
4444
|
+
let eventType = "non-json";
|
|
4445
|
+
try {
|
|
4446
|
+
const parsed = JSON.parse(text5);
|
|
4447
|
+
if (typeof parsed.type === "string") eventType = parsed.type;
|
|
4448
|
+
if (eventType === "response.completed" || eventType === "response.failed" || eventType === "response.incomplete") {
|
|
4449
|
+
nativeCompleted = true;
|
|
4450
|
+
}
|
|
4451
|
+
} catch {
|
|
4452
|
+
}
|
|
4453
|
+
if (debug && (nativeFrameCount <= 3 || nativeCompleted || eventType === "error" || nativeFrameCount % 25 === 0)) {
|
|
4454
|
+
log14(`WS native frame#${nativeFrameCount}: model=${modelId} type=${eventType} bytes=${text5.length}`);
|
|
4455
|
+
}
|
|
4456
|
+
socket.write(wsEncodeTextFrame(text5));
|
|
4457
|
+
});
|
|
4458
|
+
upstream.once("error", (err) => closeBoth(`Native Codex WebSocket error: ${err.message}`));
|
|
4459
|
+
upstream.once("close", (code, reason) => {
|
|
4460
|
+
const detail = reason?.length ? ` reason=${reason.toString("utf8").slice(0, 200)}` : "";
|
|
4461
|
+
if (debug) log14(`WS native upstream close: model=${modelId} code=${code}${detail} frames=${nativeFrameCount}`);
|
|
4462
|
+
if (nativeUpstream === upstream) nativeUpstream = void 0;
|
|
4463
|
+
nativeActive = false;
|
|
4464
|
+
if (!finished) closeBoth(nativeCompleted ? void 0 : `Native Codex WebSocket closed before completion (${code})`);
|
|
4465
|
+
});
|
|
4466
|
+
socket.once("close", () => {
|
|
4467
|
+
if (debug) log14(`WS native downstream close: model=${modelId} frames=${nativeFrameCount} completed=${nativeCompleted}`);
|
|
4468
|
+
finished = true;
|
|
4469
|
+
nativeActive = false;
|
|
4470
|
+
if (nativeUpstream === upstream) nativeUpstream = void 0;
|
|
4471
|
+
clearTimers();
|
|
4472
|
+
try {
|
|
4473
|
+
upstream?.close();
|
|
4474
|
+
} catch {
|
|
4475
|
+
}
|
|
4476
|
+
});
|
|
4477
|
+
} catch (err) {
|
|
4478
|
+
closeBoth(`Native Codex WebSocket setup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4479
|
+
}
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
let resolved = subagentRoute ? resolveModel(routes, models, subagentRoute.modelId) : resolveModel(routes, models, modelId);
|
|
3614
4485
|
if (!resolved) {
|
|
3615
4486
|
const fb = routes[0];
|
|
3616
4487
|
const fbLm = fb ? models.get(fb.modelId) : void 0;
|
|
@@ -3629,8 +4500,13 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
|
|
|
3629
4500
|
}
|
|
3630
4501
|
const { route, languageModel } = resolved;
|
|
3631
4502
|
try {
|
|
4503
|
+
const routedBody = await prepareExternalCodexBody(body, {
|
|
4504
|
+
relay: nativePayloadRelay,
|
|
4505
|
+
mixedNative,
|
|
4506
|
+
headers: req.headers
|
|
4507
|
+
});
|
|
3632
4508
|
let params = applyClaudeCodeOAuthIdentity(route, translateResponsesRequest(
|
|
3633
|
-
|
|
4509
|
+
routedBody,
|
|
3634
4510
|
route.npm,
|
|
3635
4511
|
{
|
|
3636
4512
|
providerId: route.providerId,
|
|
@@ -3662,131 +4538,62 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
|
|
|
3662
4538
|
log14(`WS model=${route.modelId} effort=${effort ?? "(none)"} providerOptions=${JSON.stringify(params.providerOptions)}`);
|
|
3663
4539
|
}
|
|
3664
4540
|
if (v2Compaction) {
|
|
3665
|
-
await streamCompactionResponse(languageModel, params, modelId, sendWsEvent);
|
|
3666
|
-
} else
|
|
3667
|
-
await streamResponsesResponse(languageModel, params, modelId, sendWsEvent, (summary) => {
|
|
3668
|
-
if (debug) {
|
|
3669
|
-
const failure = `${summary.aborted ? " aborted=yes" : ""}${summary.errorMessage ? ` error=${JSON.stringify(summary.errorMessage)}` : ""}`;
|
|
3670
|
-
log14(`WS response done: model=${route.modelId} reasoningChars=${summary.reasoningChars} textChars=${summary.textChars} toolCalls=${summary.toolCallCount} toolNames=[${summary.toolNames.join(",")}] loopDetected=${summary.loopDetected ?? "no"} dsmlRecovered=${summary.dsmlToolCallsRecovered ?? 0}${failure} reasoningPreview=${JSON.stringify(summary.reasoningPreview)}`);
|
|
3671
|
-
}
|
|
3672
|
-
}, (progress) => {
|
|
3673
|
-
if (debug) {
|
|
3674
|
-
log14(`WS response progress: model=${route.modelId} elapsedMs=${progress.elapsedMs} reasoningChars=${progress.reasoningChars} textChars=${progress.textChars} toolCalls=${progress.toolCallCount} reasoningTail=${JSON.stringify(progress.reasoningTail)}`);
|
|
3675
|
-
}
|
|
3676
|
-
});
|
|
3677
|
-
} catch (err) {
|
|
3678
|
-
const msg = formatUpstreamError(err);
|
|
3679
|
-
const status = upstreamHttpStatus(err, msg);
|
|
3680
|
-
if (debug) log14(`WS sdk error: ${route.modelId}: ${msg}`);
|
|
3681
|
-
if (status === 429) {
|
|
3682
|
-
writeResponsesRateLimitStream(modelId, msg, sendWsEvent);
|
|
3683
|
-
} else {
|
|
3684
|
-
writeResponsesErrorStream(modelId, msg, sendWsEvent, status);
|
|
3685
|
-
}
|
|
3686
|
-
}
|
|
3687
|
-
closeSocket();
|
|
3688
|
-
})();
|
|
3689
|
-
};
|
|
3690
|
-
socket.on("error", () => socket.destroy());
|
|
3691
|
-
socket.on("data", onData);
|
|
3692
|
-
onData(head);
|
|
3693
|
-
});
|
|
3694
|
-
server.keepAliveTimeout = 0;
|
|
3695
|
-
server.headersTimeout = 0;
|
|
3696
|
-
server.on("error", reject2);
|
|
3697
|
-
server.listen(0, "127.0.0.1", () => {
|
|
3698
|
-
const addr = server.address();
|
|
3699
|
-
if (!addr || typeof addr === "string") {
|
|
3700
|
-
reject2(new Error("Failed to bind codex proxy"));
|
|
3701
|
-
return;
|
|
3702
|
-
}
|
|
3703
|
-
resolve2({
|
|
3704
|
-
port: addr.port,
|
|
3705
|
-
close: () => {
|
|
3706
|
-
process.off("unhandledRejection", onRejection);
|
|
3707
|
-
server.close();
|
|
3708
|
-
}
|
|
3709
|
-
});
|
|
3710
|
-
});
|
|
3711
|
-
});
|
|
3712
|
-
}
|
|
3713
|
-
|
|
3714
|
-
// src/codex/profile.ts
|
|
3715
|
-
import { join as join3 } from "path";
|
|
3716
|
-
|
|
3717
|
-
// src/codex/routing.ts
|
|
3718
|
-
function codexCompatibleProviders(providers, agent = "codex") {
|
|
3719
|
-
return providersForTarget(providers, agent);
|
|
3720
|
-
}
|
|
3721
|
-
function resolveBaseURL(model, provider) {
|
|
3722
|
-
if (provider.id === "zen" || provider.id === "go") {
|
|
3723
|
-
const isAnthropic = model.modelFormat === "anthropic";
|
|
3724
|
-
const baseUrl = BACKENDS[provider.id].baseUrl;
|
|
3725
|
-
return isAnthropic ? baseUrl : `${baseUrl}/v1`;
|
|
3726
|
-
}
|
|
3727
|
-
return model.apiBaseUrl ?? model.completionsUrl?.replace(/\/chat\/completions$/, "") ?? model.baseUrl;
|
|
3728
|
-
}
|
|
3729
|
-
function resolveCodexRoute(provider, model, apiKey) {
|
|
3730
|
-
const upstreamModelId = model.upstreamModelId || model.id;
|
|
3731
|
-
const inferredNpm = model.modelFormat === "anthropic" ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible";
|
|
3732
|
-
const isZenGo = provider.id === "zen" || provider.id === "go";
|
|
3733
|
-
const base = {
|
|
3734
|
-
npm: isZenGo ? inferredNpm : model.npm ?? inferredNpm,
|
|
3735
|
-
baseURL: resolveBaseURL(model, provider),
|
|
3736
|
-
upstreamModelId,
|
|
3737
|
-
apiKey,
|
|
3738
|
-
contextWindow: model.contextWindow,
|
|
3739
|
-
modelId: model.id,
|
|
3740
|
-
providerId: provider.id,
|
|
3741
|
-
authType: provider.authType,
|
|
3742
|
-
oauthAccountId: provider.oauthAccountId,
|
|
3743
|
-
providerData: provider.providerData,
|
|
3744
|
-
supportedParameters: model.supportedParameters,
|
|
3745
|
-
reasoning: model.reasoning,
|
|
3746
|
-
interleavedReasoningField: model.interleavedReasoningField,
|
|
3747
|
-
headers: provider.headers
|
|
3748
|
-
};
|
|
3749
|
-
if (model.modelFormat === "cloud-code") {
|
|
3750
|
-
return {
|
|
3751
|
-
tier: "cloud-code",
|
|
3752
|
-
npm: "@ai-sdk/anthropic",
|
|
3753
|
-
baseURL: "",
|
|
3754
|
-
upstreamModelId: model.upstreamModelId || model.id,
|
|
3755
|
-
apiKey,
|
|
3756
|
-
contextWindow: model.contextWindow,
|
|
3757
|
-
modelId: model.id,
|
|
3758
|
-
providerId: provider.id,
|
|
3759
|
-
authType: provider.authType,
|
|
3760
|
-
oauthAccountId: provider.oauthAccountId,
|
|
3761
|
-
providerData: provider.providerData,
|
|
3762
|
-
supportedParameters: model.supportedParameters,
|
|
3763
|
-
reasoning: model.reasoning,
|
|
3764
|
-
interleavedReasoningField: model.interleavedReasoningField,
|
|
3765
|
-
headers: provider.headers
|
|
3766
|
-
};
|
|
3767
|
-
}
|
|
3768
|
-
if (model.npm === "@ai-sdk/openai" && provider.authType !== "oauth" && model.modelFormat === "openai") {
|
|
3769
|
-
return { tier: "direct", ...base };
|
|
3770
|
-
}
|
|
3771
|
-
return { tier: "proxy", ...base };
|
|
3772
|
-
}
|
|
3773
|
-
function routableModelsForProvider(provider, agent = "codex") {
|
|
3774
|
-
return routableModelsForTarget(provider, agent);
|
|
3775
|
-
}
|
|
3776
|
-
function codexProviderEnvKey(providerId) {
|
|
3777
|
-
const known = {
|
|
3778
|
-
openai: "OPENAI_API_KEY",
|
|
3779
|
-
xai: "XAI_API_KEY",
|
|
3780
|
-
"xai-oauth": "XAI_API_KEY",
|
|
3781
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
3782
|
-
google: "GEMINI_API_KEY"
|
|
3783
|
-
};
|
|
3784
|
-
return known[providerId] ?? `${providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
4541
|
+
await streamCompactionResponse(languageModel, params, modelId, sendWsEvent);
|
|
4542
|
+
} else
|
|
4543
|
+
await streamResponsesResponse(languageModel, params, modelId, sendWsEvent, (summary) => {
|
|
4544
|
+
if (debug) {
|
|
4545
|
+
const failure = `${summary.aborted ? " aborted=yes" : ""}${summary.errorMessage ? ` error=${JSON.stringify(summary.errorMessage)}` : ""}`;
|
|
4546
|
+
log14(`WS response done: model=${route.modelId} reasoningChars=${summary.reasoningChars} textChars=${summary.textChars} toolCalls=${summary.toolCallCount} toolNames=[${summary.toolNames.join(",")}] loopDetected=${summary.loopDetected ?? "no"} dsmlRecovered=${summary.dsmlToolCallsRecovered ?? 0}${failure} reasoningPreview=${JSON.stringify(summary.reasoningPreview)}`);
|
|
4547
|
+
}
|
|
4548
|
+
}, (progress) => {
|
|
4549
|
+
if (debug) {
|
|
4550
|
+
log14(`WS response progress: model=${route.modelId} elapsedMs=${progress.elapsedMs} reasoningChars=${progress.reasoningChars} textChars=${progress.textChars} toolCalls=${progress.toolCallCount} reasoningTail=${JSON.stringify(progress.reasoningTail)}`);
|
|
4551
|
+
}
|
|
4552
|
+
});
|
|
4553
|
+
} catch (err) {
|
|
4554
|
+
const msg = formatUpstreamError(err);
|
|
4555
|
+
const status = upstreamHttpStatus(err, msg);
|
|
4556
|
+
if (debug) log14(`WS sdk error: ${route.modelId}: ${msg}`);
|
|
4557
|
+
if (status === 429) {
|
|
4558
|
+
writeResponsesRateLimitStream(modelId, msg, sendWsEvent);
|
|
4559
|
+
} else {
|
|
4560
|
+
writeResponsesErrorStream(modelId, msg, sendWsEvent, status);
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
closeSocket();
|
|
4564
|
+
})();
|
|
4565
|
+
};
|
|
4566
|
+
socket.on("error", () => socket.destroy());
|
|
4567
|
+
socket.on("data", onData);
|
|
4568
|
+
onData(head);
|
|
4569
|
+
});
|
|
4570
|
+
server.keepAliveTimeout = 0;
|
|
4571
|
+
server.headersTimeout = 0;
|
|
4572
|
+
server.on("error", reject2);
|
|
4573
|
+
server.listen(0, "127.0.0.1", () => {
|
|
4574
|
+
const addr = server.address();
|
|
4575
|
+
if (!addr || typeof addr === "string") {
|
|
4576
|
+
reject2(new Error("Failed to bind codex proxy"));
|
|
4577
|
+
return;
|
|
4578
|
+
}
|
|
4579
|
+
resolve2({
|
|
4580
|
+
port: addr.port,
|
|
4581
|
+
close: () => {
|
|
4582
|
+
process.off("unhandledRejection", onRejection);
|
|
4583
|
+
server.close();
|
|
4584
|
+
}
|
|
4585
|
+
});
|
|
4586
|
+
});
|
|
4587
|
+
});
|
|
3785
4588
|
}
|
|
3786
4589
|
|
|
4590
|
+
// src/codex/profile.ts
|
|
4591
|
+
import { join as join3 } from "path";
|
|
4592
|
+
|
|
3787
4593
|
// src/codex/session.ts
|
|
3788
4594
|
import {
|
|
3789
4595
|
copyFileSync,
|
|
4596
|
+
chmodSync,
|
|
3790
4597
|
existsSync as existsSync3,
|
|
3791
4598
|
mkdirSync,
|
|
3792
4599
|
readdirSync,
|
|
@@ -3802,8 +4609,8 @@ import { basename, dirname, join as join2 } from "path";
|
|
|
3802
4609
|
var CODEX_PROFILE_NAME = "relay-ai-launch";
|
|
3803
4610
|
var STALE_SESSION_MS = 5 * 60 * 1e3;
|
|
3804
4611
|
var MAX_BACKUPS = 5;
|
|
3805
|
-
function getCodexHome() {
|
|
3806
|
-
return join2(homedir3(), ".codex");
|
|
4612
|
+
function getCodexHome(env = process.env) {
|
|
4613
|
+
return env["CODEX_HOME"] || join2(homedir3(), ".codex");
|
|
3807
4614
|
}
|
|
3808
4615
|
function getCodexProfilePath() {
|
|
3809
4616
|
return join2(getCodexHome(), `${CODEX_PROFILE_NAME}.config.toml`);
|
|
@@ -3821,7 +4628,7 @@ function getCatalogPath(providerId, env = process.env) {
|
|
|
3821
4628
|
return join2(getRelayAiCodexDir(env), `models-${providerId}.json`);
|
|
3822
4629
|
}
|
|
3823
4630
|
function ownedOverlayPaths(env = process.env) {
|
|
3824
|
-
const paths = [getCodexProfilePath()
|
|
4631
|
+
const paths = [getCodexProfilePath()];
|
|
3825
4632
|
const codexDir = getRelayAiCodexDir(env);
|
|
3826
4633
|
if (existsSync3(codexDir)) {
|
|
3827
4634
|
for (const name of readdirSync(codexDir)) {
|
|
@@ -3830,13 +4637,26 @@ function ownedOverlayPaths(env = process.env) {
|
|
|
3830
4637
|
}
|
|
3831
4638
|
}
|
|
3832
4639
|
}
|
|
4640
|
+
const agentsDir = join2(getCodexHome(env), "agents");
|
|
4641
|
+
if (existsSync3(agentsDir)) {
|
|
4642
|
+
for (const name of readdirSync(agentsDir)) {
|
|
4643
|
+
if (/^relay-model-[a-z0-9-]+\.toml$/i.test(name)) {
|
|
4644
|
+
paths.push(join2(agentsDir, name));
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
paths.push(getSessionLockPath(env));
|
|
3833
4649
|
return paths;
|
|
3834
4650
|
}
|
|
3835
4651
|
function atomicWriteFile(path3, content) {
|
|
3836
4652
|
mkdirSync(dirname(path3), { recursive: true });
|
|
3837
4653
|
const tmp = `${path3}.tmp.${process.pid}`;
|
|
3838
|
-
writeFileSync(tmp, content, "utf8");
|
|
4654
|
+
writeFileSync(tmp, content, { encoding: "utf8", mode: 384 });
|
|
3839
4655
|
renameSync(tmp, path3);
|
|
4656
|
+
try {
|
|
4657
|
+
chmodSync(path3, 384);
|
|
4658
|
+
} catch {
|
|
4659
|
+
}
|
|
3840
4660
|
}
|
|
3841
4661
|
function rotateBackups(filePath, env = process.env) {
|
|
3842
4662
|
if (!existsSync3(filePath)) return;
|
|
@@ -3969,6 +4789,23 @@ env_key = "RELAY_AI_CODEX_KEY"
|
|
|
3969
4789
|
wire_api = "responses"
|
|
3970
4790
|
`;
|
|
3971
4791
|
}
|
|
4792
|
+
function buildCodexMixedProfileToml(spec) {
|
|
4793
|
+
const multiAgentV2 = spec.multiAgentV2Enabled ? `${renderMultiAgentV2Feature()}
|
|
4794
|
+
` : "";
|
|
4795
|
+
return `# Generated by relay-ai \u2014 do not edit
|
|
4796
|
+
${profileSandboxLine()}model = ${tomlString(spec.model)}
|
|
4797
|
+
model_provider = "openai"
|
|
4798
|
+
openai_base_url = ${tomlString(spec.baseUrl)}
|
|
4799
|
+
model_catalog_json = ${tomlString(spec.catalogPath)}
|
|
4800
|
+
|
|
4801
|
+
${multiAgentV2}
|
|
4802
|
+
[model_providers.relay-ai]
|
|
4803
|
+
name = "Relay AI"
|
|
4804
|
+
base_url = ${tomlString(spec.baseUrl)}
|
|
4805
|
+
wire_api = "responses"
|
|
4806
|
+
env_key = "RELAY_AI_CODEX_KEY"
|
|
4807
|
+
`;
|
|
4808
|
+
}
|
|
3972
4809
|
function getProfileOutputPath() {
|
|
3973
4810
|
return getCodexProfilePath();
|
|
3974
4811
|
}
|
|
@@ -4073,9 +4910,12 @@ function ensureCodexSandboxArgs(extraArgs) {
|
|
|
4073
4910
|
if (codexArgsIncludeSandboxFlag(extraArgs)) return extraArgs;
|
|
4074
4911
|
return ["-s", CODEX_LAUNCH_SANDBOX, ...extraArgs];
|
|
4075
4912
|
}
|
|
4076
|
-
function buildCodexChildEnv(route, proxyPort) {
|
|
4913
|
+
function buildCodexChildEnv(route, proxyPort, options = {}) {
|
|
4077
4914
|
const env = stripCodexInheritedEnv(process.env);
|
|
4078
|
-
if (
|
|
4915
|
+
if (options.mixedNative) {
|
|
4916
|
+
env["RELAY_AI_CODEX_KEY"] = PROXY_PLACEHOLDER_KEY;
|
|
4917
|
+
delete env[codexProviderEnvKey(route.providerId)];
|
|
4918
|
+
} else if (route.tier === "proxy" && proxyPort) {
|
|
4079
4919
|
env["RELAY_AI_CODEX_KEY"] = PROXY_PLACEHOLDER_KEY;
|
|
4080
4920
|
} else {
|
|
4081
4921
|
const envKey = codexProviderEnvKey(route.providerId);
|
|
@@ -4104,6 +4944,32 @@ function launchCodex(modelId, env, extraArgs) {
|
|
|
4104
4944
|
// src/codex/prompts.ts
|
|
4105
4945
|
import pc5 from "picocolors";
|
|
4106
4946
|
import * as p6 from "@clack/prompts";
|
|
4947
|
+
function codexLaunchModeOptions() {
|
|
4948
|
+
return [
|
|
4949
|
+
{
|
|
4950
|
+
value: "relay-only",
|
|
4951
|
+
label: "Relay models only",
|
|
4952
|
+
hint: "Keep native Codex models hidden for this launch"
|
|
4953
|
+
},
|
|
4954
|
+
{
|
|
4955
|
+
value: "mixed",
|
|
4956
|
+
label: "Relay + native Codex models",
|
|
4957
|
+
hint: "Expose native Codex models alongside your Relay catalog"
|
|
4958
|
+
}
|
|
4959
|
+
];
|
|
4960
|
+
}
|
|
4961
|
+
async function pickCodexLaunchMode() {
|
|
4962
|
+
const choice = await p6.select({
|
|
4963
|
+
message: "Load native Codex models alongside Relay models?",
|
|
4964
|
+
options: codexLaunchModeOptions(),
|
|
4965
|
+
initialValue: "relay-only"
|
|
4966
|
+
});
|
|
4967
|
+
if (p6.isCancel(choice)) {
|
|
4968
|
+
p6.cancel("Cancelled.");
|
|
4969
|
+
return null;
|
|
4970
|
+
}
|
|
4971
|
+
return choice;
|
|
4972
|
+
}
|
|
4107
4973
|
async function pickCodexProvider(providers, prefs, hasFavorites = false, initialProviderId) {
|
|
4108
4974
|
if (providers.length === 0 && !hasFavorites) return null;
|
|
4109
4975
|
const options = providers.map((lp) => providerSelectOption(lp));
|
|
@@ -4304,7 +5170,9 @@ async function resolveFavorite(fav, ctx) {
|
|
|
4304
5170
|
apiKey: await resolveLocalProviderApiKey(found.provider) ?? "",
|
|
4305
5171
|
authType: found.provider.authType,
|
|
4306
5172
|
oauthAccountId: found.provider.oauthAccountId,
|
|
4307
|
-
providerData: found.provider.providerData
|
|
5173
|
+
providerData: found.provider.providerData,
|
|
5174
|
+
headers: found.provider.headers,
|
|
5175
|
+
refreshToken: providerRefreshToken(found.provider.id, found.provider.authType, found.provider.authRef)
|
|
4308
5176
|
};
|
|
4309
5177
|
}
|
|
4310
5178
|
return void 0;
|
|
@@ -4454,6 +5322,136 @@ async function resolveCodexFavorites(activeProvider, selectedModel, compatible,
|
|
|
4454
5322
|
providersById: new Map(compatible.map((lp) => [lp.id, lp]))
|
|
4455
5323
|
};
|
|
4456
5324
|
}
|
|
5325
|
+
function assertConfiguredCodexSubagentsResolved(configured, resolved) {
|
|
5326
|
+
const resolvedKeys = new Set(
|
|
5327
|
+
resolved.subagents.map((entry) => `${entry.providerId}:${entry.model.id}`)
|
|
5328
|
+
);
|
|
5329
|
+
const missing = configured.filter((entry) => !resolvedKeys.has(`${entry.providerId}:${entry.modelId}`));
|
|
5330
|
+
if (missing.length === 0) return;
|
|
5331
|
+
throw new Error(
|
|
5332
|
+
`Configured Codex Sub-agent model(s) are unavailable for this launch: ${missing.map((entry) => `${entry.providerId}:${entry.modelId}`).join(", ")}. Check the provider credential and refresh the provider model catalog. Mixed mode was not started.`
|
|
5333
|
+
);
|
|
5334
|
+
}
|
|
5335
|
+
async function resolveCodexMixedModels(input) {
|
|
5336
|
+
const ctx = {
|
|
5337
|
+
agent: "codex",
|
|
5338
|
+
localProviders: input.compatible,
|
|
5339
|
+
findLocalModel: (providerId, modelId) => {
|
|
5340
|
+
const provider = input.compatible.find((lp) => lp.id === providerId);
|
|
5341
|
+
const model = provider?.models.find((m) => m.id === modelId);
|
|
5342
|
+
return provider && model ? { provider, model } : void 0;
|
|
5343
|
+
}
|
|
5344
|
+
};
|
|
5345
|
+
const selected = await resolveFavorite(
|
|
5346
|
+
{ providerId: input.activeProvider.id, modelId: input.selectedModel.id },
|
|
5347
|
+
ctx
|
|
5348
|
+
);
|
|
5349
|
+
if (!selected) throw new Error("Selected Codex model is no longer available");
|
|
5350
|
+
const visibleResult = await buildFavoritesList(selected, input.generalFavorites, ctx, 20, { trackCapacitySkipped: true });
|
|
5351
|
+
const subagentResult = await buildFavoritesList(void 0, input.subagentFavorites, ctx, 20, { trackCapacitySkipped: true });
|
|
5352
|
+
const all = [...visibleResult.resolved];
|
|
5353
|
+
for (const entry of subagentResult.resolved) {
|
|
5354
|
+
const key = `${entry.providerId}\0${entry.model.id}`;
|
|
5355
|
+
if (!all.some((existing) => `${existing.providerId}\0${existing.model.id}` === key)) all.push(entry);
|
|
5356
|
+
}
|
|
5357
|
+
return {
|
|
5358
|
+
selected,
|
|
5359
|
+
visible: visibleResult.resolved,
|
|
5360
|
+
subagents: subagentResult.resolved,
|
|
5361
|
+
all,
|
|
5362
|
+
providersById: new Map(input.compatible.map((provider) => [provider.id, provider])),
|
|
5363
|
+
dropped: [...visibleResult.droppedFavorites, ...subagentResult.droppedFavorites]
|
|
5364
|
+
};
|
|
5365
|
+
}
|
|
5366
|
+
|
|
5367
|
+
// src/codex/native-catalog.ts
|
|
5368
|
+
import { execFile } from "child_process";
|
|
5369
|
+
import { promisify } from "util";
|
|
5370
|
+
var execFileAsync = promisify(execFile);
|
|
5371
|
+
function isCatalogModel(value) {
|
|
5372
|
+
if (!value || typeof value !== "object") return false;
|
|
5373
|
+
const model = value;
|
|
5374
|
+
return typeof model.slug === "string" && model.slug.length > 0 && typeof model.visibility === "string" && typeof model.display_name === "string";
|
|
5375
|
+
}
|
|
5376
|
+
function validateNativeCodexCatalog(value) {
|
|
5377
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.models)) {
|
|
5378
|
+
throw new Error("Invalid native Codex catalog: expected a models array");
|
|
5379
|
+
}
|
|
5380
|
+
const models = value.models;
|
|
5381
|
+
if (models.length === 0) throw new Error("Invalid native Codex catalog: no models");
|
|
5382
|
+
if (!models.every(isCatalogModel)) throw new Error("Invalid native Codex catalog: invalid model entry");
|
|
5383
|
+
return { models };
|
|
5384
|
+
}
|
|
5385
|
+
async function captureNativeCodexCatalog(options) {
|
|
5386
|
+
const run = options.run ?? (async (args) => {
|
|
5387
|
+
const result = await execFileAsync(options.binaryPath, args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
|
|
5388
|
+
return result.stdout;
|
|
5389
|
+
});
|
|
5390
|
+
const stdout = await run(options.bundled ? ["debug", "models", "--bundled"] : ["debug", "models"]);
|
|
5391
|
+
let parsed;
|
|
5392
|
+
try {
|
|
5393
|
+
parsed = JSON.parse(stdout);
|
|
5394
|
+
} catch {
|
|
5395
|
+
throw new Error("Native Codex catalog was not valid JSON");
|
|
5396
|
+
}
|
|
5397
|
+
const catalog = validateNativeCodexCatalog(parsed);
|
|
5398
|
+
return {
|
|
5399
|
+
schemaVersion: 1,
|
|
5400
|
+
target: options.target,
|
|
5401
|
+
binaryPath: options.binaryPath,
|
|
5402
|
+
codexVersion: options.codexVersion,
|
|
5403
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5404
|
+
source: options.bundled ? "bundled" : "refreshed",
|
|
5405
|
+
models: catalog.models
|
|
5406
|
+
};
|
|
5407
|
+
}
|
|
5408
|
+
|
|
5409
|
+
// src/codex/mixed-catalog.ts
|
|
5410
|
+
function externalCatalogEntryFromTemplate(template, entry, priority, visibility, multiAgentVersion) {
|
|
5411
|
+
const resolvedModel = entry.resolved.model;
|
|
5412
|
+
const generated = catalogEntryFromModel(
|
|
5413
|
+
resolvedModel,
|
|
5414
|
+
entry.resolved.providerName,
|
|
5415
|
+
priority,
|
|
5416
|
+
false,
|
|
5417
|
+
entry.slug
|
|
5418
|
+
);
|
|
5419
|
+
return {
|
|
5420
|
+
...template,
|
|
5421
|
+
...generated,
|
|
5422
|
+
slug: entry.slug,
|
|
5423
|
+
display_name: `${generated.display_name} \xB7 ${entry.resolved.providerName}`,
|
|
5424
|
+
visibility,
|
|
5425
|
+
multi_agent_version: multiAgentVersion
|
|
5426
|
+
};
|
|
5427
|
+
}
|
|
5428
|
+
function composeMixedCodexCatalog(input) {
|
|
5429
|
+
const template = input.nativeModels.find((model) => model.slug === "gpt-5.5") ?? input.nativeModels.find((model) => model.visibility === "list") ?? input.nativeModels[0];
|
|
5430
|
+
if (!template) throw new Error("Native Codex catalog has no template model");
|
|
5431
|
+
const hasSubagents = input.subagentRelay.length > 0;
|
|
5432
|
+
const models = input.nativeModels.map((model) => {
|
|
5433
|
+
if (!hasSubagents || model.visibility !== "list" || model.multi_agent_version === "disabled") return model;
|
|
5434
|
+
return { ...model, multi_agent_version: input.externalMultiAgentVersion };
|
|
5435
|
+
});
|
|
5436
|
+
const added = new Set(models.map((model) => model.slug));
|
|
5437
|
+
const visible = [...input.visibleRelay].sort((a, b) => (a.slug === input.selectedSlug ? -1 : 0) - (b.slug === input.selectedSlug ? -1 : 0));
|
|
5438
|
+
const subagentSlugs = new Set(input.subagentRelay.map((entry) => entry.slug));
|
|
5439
|
+
for (const entry of [...visible, ...input.subagentRelay]) {
|
|
5440
|
+
if (added.has(entry.slug)) continue;
|
|
5441
|
+
added.add(entry.slug);
|
|
5442
|
+
models.push(externalCatalogEntryFromTemplate(
|
|
5443
|
+
template,
|
|
5444
|
+
entry,
|
|
5445
|
+
entry.slug === input.selectedSlug ? 0 : models.length,
|
|
5446
|
+
"list",
|
|
5447
|
+
hasSubagents || subagentSlugs.has(entry.slug) ? input.externalMultiAgentVersion : "v1"
|
|
5448
|
+
));
|
|
5449
|
+
}
|
|
5450
|
+
return { models };
|
|
5451
|
+
}
|
|
5452
|
+
function mixedRelaySlug(providerId, modelId) {
|
|
5453
|
+
return codexCliFavoritesSlug(providerId, modelId);
|
|
5454
|
+
}
|
|
4457
5455
|
|
|
4458
5456
|
// src/cloud-code-backend.ts
|
|
4459
5457
|
function needsCloudCodeBackend(model, authType) {
|
|
@@ -4518,6 +5516,93 @@ async function startCloudCodeCatalogBackend(routes, startingAliasId, trace) {
|
|
|
4518
5516
|
return { port: handle.port, token: handle.token, handle };
|
|
4519
5517
|
}
|
|
4520
5518
|
|
|
5519
|
+
// src/codex/mixed-launch.ts
|
|
5520
|
+
async function prepareCodexMixedRelayRoutes(models, trace = false) {
|
|
5521
|
+
const backendResolved = models.all.filter((entry) => {
|
|
5522
|
+
const provider = models.providersById.get(entry.providerId);
|
|
5523
|
+
return needsCloudCodeBackend(entry.model, provider?.authType);
|
|
5524
|
+
});
|
|
5525
|
+
const regularResolved = models.all.filter((entry) => !backendResolved.includes(entry));
|
|
5526
|
+
let cloudCodeBackend = null;
|
|
5527
|
+
let backendRoutes = [];
|
|
5528
|
+
if (backendResolved.length > 0) {
|
|
5529
|
+
const partitioned = await partitionAndStartCloudCodeBackend(
|
|
5530
|
+
backendResolved.map((entry) => {
|
|
5531
|
+
const provider = models.providersById.get(entry.providerId);
|
|
5532
|
+
if (!provider) throw new Error(`Provider ${entry.providerId} is unavailable for mixed Codex mode`);
|
|
5533
|
+
return {
|
|
5534
|
+
providerId: entry.providerId,
|
|
5535
|
+
model: entry.model,
|
|
5536
|
+
apiKey: entry.apiKey,
|
|
5537
|
+
oauthAccountId: provider.oauthAccountId,
|
|
5538
|
+
providerData: provider.providerData ?? {}
|
|
5539
|
+
};
|
|
5540
|
+
}),
|
|
5541
|
+
(proxyRoute, backend, original) => ({
|
|
5542
|
+
modelId: codexCliFavoritesSlug(original.providerId, original.model.id),
|
|
5543
|
+
npm: "@ai-sdk/anthropic",
|
|
5544
|
+
apiKey: backend.token,
|
|
5545
|
+
baseURL: `http://127.0.0.1:${backend.port}`,
|
|
5546
|
+
upstreamModelId: proxyRoute.aliasId,
|
|
5547
|
+
providerId: original.providerId,
|
|
5548
|
+
authType: "oauth",
|
|
5549
|
+
oauthAccountId: original.oauthAccountId,
|
|
5550
|
+
providerData: original.providerData,
|
|
5551
|
+
contextWindow: proxyRoute.contextWindow
|
|
5552
|
+
}),
|
|
5553
|
+
trace
|
|
5554
|
+
);
|
|
5555
|
+
cloudCodeBackend = partitioned.backend;
|
|
5556
|
+
backendRoutes = partitioned.backendItems;
|
|
5557
|
+
}
|
|
5558
|
+
return {
|
|
5559
|
+
routes: [...backendRoutes, ...buildCodexProxyRoutesFromResolved(regularResolved, models.providersById)],
|
|
5560
|
+
cloudCodeBackend
|
|
5561
|
+
};
|
|
5562
|
+
}
|
|
5563
|
+
function selectNativePayloadRelayModel(models) {
|
|
5564
|
+
for (const preferred of ["gpt-5.4-mini", "gpt-5.4"]) {
|
|
5565
|
+
if (models.some((model) => model.slug === preferred)) return preferred;
|
|
5566
|
+
}
|
|
5567
|
+
const fallback = models.find((model) => model.visibility === "list" && model.multi_agent_version !== "disabled");
|
|
5568
|
+
if (!fallback) throw new Error("Mixed Codex mode requires one native model for collaboration payload relay");
|
|
5569
|
+
return fallback.slug;
|
|
5570
|
+
}
|
|
5571
|
+
function buildCodexMixedLaunchPlan(input) {
|
|
5572
|
+
const relayRoutes = input.relayRoutes ?? buildCodexProxyRoutesFromResolved(input.models.all, input.models.providersById);
|
|
5573
|
+
const routeByKey = new Set(relayRoutes.map((route) => route.modelId));
|
|
5574
|
+
const visibleRelay = input.models.visible.map((resolved) => ({ resolved, slug: mixedRelaySlug(resolved.providerId, resolved.model.id) })).filter((entry) => routeByKey.has(entry.slug));
|
|
5575
|
+
const subagentRelay = input.models.subagents.map((resolved) => ({ resolved, slug: mixedRelaySlug(resolved.providerId, resolved.model.id) })).filter((entry) => routeByKey.has(entry.slug));
|
|
5576
|
+
const selectedSlug = codexCliFavoritesSlug(input.models.selected.providerId, input.models.selected.model.id);
|
|
5577
|
+
const hasSubagents = input.models.subagents.length > 0;
|
|
5578
|
+
if (input.models.subagents.length > 1) {
|
|
5579
|
+
throw new Error("Codex mixed mode supports exactly one Relay Sub-agent model");
|
|
5580
|
+
}
|
|
5581
|
+
if (hasSubagents && input.multiAgentV2Supported === false) {
|
|
5582
|
+
throw new Error("Configured Codex Sub-agents require a Codex runtime with multi_agent_v2 support");
|
|
5583
|
+
}
|
|
5584
|
+
const multiAgent = input.nativeCatalog.models.some((model) => model.multi_agent_version === "v2") ? "v2" : "v1";
|
|
5585
|
+
const multiAgentV2Enabled = hasSubagents && input.multiAgentV2Supported === true;
|
|
5586
|
+
return {
|
|
5587
|
+
selectedSlug,
|
|
5588
|
+
nativeCatalog: input.nativeCatalog,
|
|
5589
|
+
catalog: composeMixedCodexCatalog({
|
|
5590
|
+
nativeModels: input.nativeCatalog.models,
|
|
5591
|
+
visibleRelay,
|
|
5592
|
+
subagentRelay,
|
|
5593
|
+
selectedSlug,
|
|
5594
|
+
externalMultiAgentVersion: multiAgent
|
|
5595
|
+
}),
|
|
5596
|
+
relayRoutes,
|
|
5597
|
+
nativeModelIds: new Set(input.nativeCatalog.models.map((model) => model.slug)),
|
|
5598
|
+
subagentModelCount: input.models.subagents.length,
|
|
5599
|
+
subagentRouteModelId: subagentRelay[0]?.slug,
|
|
5600
|
+
multiAgentV2Enabled,
|
|
5601
|
+
nativePayloadRelayModel: selectNativePayloadRelayModel(input.nativeCatalog.models),
|
|
5602
|
+
capability: createMixedProxyCapability()
|
|
5603
|
+
};
|
|
5604
|
+
}
|
|
5605
|
+
|
|
4521
5606
|
// src/agent-io.ts
|
|
4522
5607
|
var agentStdoutMode = false;
|
|
4523
5608
|
function setAgentStdoutMode(enabled) {
|
|
@@ -4703,6 +5788,8 @@ ${pc7.bold("Options:")}
|
|
|
4703
5788
|
--provider Boot provider id (skip wizard when paired with --model or non-interactive)
|
|
4704
5789
|
--model Boot model id (skip wizard when paired with --provider or non-interactive)
|
|
4705
5790
|
--vertex Use Claude models through Google Vertex AI
|
|
5791
|
+
--with-native Load native Codex models beside Relay models for this launch
|
|
5792
|
+
--relay-only Keep the current Relay-only launch behavior
|
|
4706
5793
|
--restore Remove interrupted-session overlay files
|
|
4707
5794
|
--config Preview/write launch configuration without starting Codex
|
|
4708
5795
|
--help Show this command help
|
|
@@ -4786,6 +5873,21 @@ async function writeFavoritesLaunchArtifacts(resolved, starting, proxyPort) {
|
|
|
4786
5873
|
}));
|
|
4787
5874
|
return { profilePath, catalogPath };
|
|
4788
5875
|
}
|
|
5876
|
+
async function writeMixedLaunchArtifacts(plan, proxyPort) {
|
|
5877
|
+
const catalogPath = join5(getRelayAiCodexDir(), "models-mixed.json");
|
|
5878
|
+
writeOverlayFile(catalogPath, serializeCatalog(plan.catalog));
|
|
5879
|
+
const profilePath = getProfileOutputPath();
|
|
5880
|
+
writeOverlayFile(profilePath, buildCodexMixedProfileToml({
|
|
5881
|
+
model: plan.selectedSlug,
|
|
5882
|
+
catalogPath,
|
|
5883
|
+
baseUrl: `${mixedProxyBaseUrl(proxyPort, plan.capability)}/v1`,
|
|
5884
|
+
multiAgentV2Enabled: plan.multiAgentV2Enabled
|
|
5885
|
+
}));
|
|
5886
|
+
return {
|
|
5887
|
+
profilePath,
|
|
5888
|
+
catalogPath
|
|
5889
|
+
};
|
|
5890
|
+
}
|
|
4789
5891
|
function printCodexCleanupReminder(hadProxy) {
|
|
4790
5892
|
if (isAgentStdoutMode()) return;
|
|
4791
5893
|
const left = remainingOverlayPaths();
|
|
@@ -5010,7 +6112,13 @@ Error: ${launchPlan.error}
|
|
|
5010
6112
|
return 0;
|
|
5011
6113
|
}
|
|
5012
6114
|
const favorites = prefs.favoriteModels ?? [];
|
|
5013
|
-
|
|
6115
|
+
let mixedMode = launch.codexLaunchMode === "mixed";
|
|
6116
|
+
if (!configOnly && isTty && !launchPlan.skip && launch.codexLaunchMode === void 0) {
|
|
6117
|
+
const selectedLaunchMode = await pickCodexLaunchMode();
|
|
6118
|
+
if (!selectedLaunchMode) return 0;
|
|
6119
|
+
mixedMode = selectedLaunchMode === "mixed";
|
|
6120
|
+
}
|
|
6121
|
+
const favoritesActive = favorites.length > 0 && !launchPlan.skip && !mixedMode;
|
|
5014
6122
|
if (favoritesActive && !configOnly) {
|
|
5015
6123
|
p8.log.info(
|
|
5016
6124
|
`Favorites mode active \u2014 Codex picker will show ${favorites.length + 1} models (1 starting + ${favorites.length} favorites).`
|
|
@@ -5083,6 +6191,42 @@ Error: ${launchPlan.error}
|
|
|
5083
6191
|
return 1;
|
|
5084
6192
|
}
|
|
5085
6193
|
const route = resolveCodexRoute(activeProvider, selectedModel, apiKey);
|
|
6194
|
+
let cloudCodeBackend = null;
|
|
6195
|
+
let cloudCodeBackendFav = null;
|
|
6196
|
+
let mixedPlan = null;
|
|
6197
|
+
if (mixedMode) {
|
|
6198
|
+
try {
|
|
6199
|
+
const version = execFileSync2(codexPath, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
6200
|
+
const mixedModels = await resolveCodexMixedModels({
|
|
6201
|
+
activeProvider,
|
|
6202
|
+
selectedModel,
|
|
6203
|
+
compatible,
|
|
6204
|
+
generalFavorites: favorites,
|
|
6205
|
+
subagentFavorites: prefs.codexSubagentModels ?? []
|
|
6206
|
+
});
|
|
6207
|
+
assertConfiguredCodexSubagentsResolved(prefs.codexSubagentModels ?? [], mixedModels);
|
|
6208
|
+
const multiAgentV2Supported = mixedModels.subagents.length === 0 || supportsMultiAgentV2(codexPath);
|
|
6209
|
+
if (!multiAgentV2Supported) {
|
|
6210
|
+
throw new Error("This Codex CLI does not support multi_agent_v2, which is required for the configured Codex SubAgent");
|
|
6211
|
+
}
|
|
6212
|
+
const nativeCatalog = await captureNativeCodexCatalog({ target: "cli", binaryPath: codexPath, codexVersion: version });
|
|
6213
|
+
const preparedRoutes = await prepareCodexMixedRelayRoutes(mixedModels, trace);
|
|
6214
|
+
cloudCodeBackend = preparedRoutes.cloudCodeBackend;
|
|
6215
|
+
mixedPlan = buildCodexMixedLaunchPlan({
|
|
6216
|
+
nativeCatalog,
|
|
6217
|
+
models: mixedModels,
|
|
6218
|
+
relayRoutes: preparedRoutes.routes,
|
|
6219
|
+
multiAgentV2Supported
|
|
6220
|
+
});
|
|
6221
|
+
} catch (err) {
|
|
6222
|
+
cloudCodeBackend?.handle.close();
|
|
6223
|
+
cloudCodeBackend = null;
|
|
6224
|
+
console.error(pc7.red(`
|
|
6225
|
+
Mixed Codex mode is unavailable: ${err instanceof Error ? err.message : err}`));
|
|
6226
|
+
console.error("Use relay-ai codex --relay-only to continue with Relay models.");
|
|
6227
|
+
return 1;
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
5086
6230
|
if (!configOnly && !(launchPlan.skip && launchPlan.target)) {
|
|
5087
6231
|
const modelLabel = formatCodexModelLabel(selectedModel);
|
|
5088
6232
|
const confirmed = await confirmCodexLaunch(
|
|
@@ -5094,11 +6238,21 @@ Error: ${launchPlan.error}
|
|
|
5094
6238
|
if (!confirmed) return 0;
|
|
5095
6239
|
}
|
|
5096
6240
|
let proxyHandle = null;
|
|
5097
|
-
let cloudCodeBackend = null;
|
|
5098
|
-
let cloudCodeBackendFav = null;
|
|
5099
6241
|
try {
|
|
5100
6242
|
let proxyPort;
|
|
5101
|
-
if (
|
|
6243
|
+
if (mixedPlan) {
|
|
6244
|
+
proxyHandle = await startCodexProxy(mixedPlan.relayRoutes, {
|
|
6245
|
+
requireAuth: false,
|
|
6246
|
+
debug: trace,
|
|
6247
|
+
mixedNative: {
|
|
6248
|
+
nativeModelIds: mixedPlan.nativeModelIds,
|
|
6249
|
+
subagentRouteModelId: mixedPlan.subagentRouteModelId,
|
|
6250
|
+
capability: mixedPlan.capability,
|
|
6251
|
+
nativePayloadRelayModel: mixedPlan.nativePayloadRelayModel
|
|
6252
|
+
}
|
|
6253
|
+
});
|
|
6254
|
+
proxyPort = proxyHandle.port;
|
|
6255
|
+
} else if (favoritesActive && resolvedFavorites.length > 0) {
|
|
5102
6256
|
const needsBackend = (r) => {
|
|
5103
6257
|
const m = r.model;
|
|
5104
6258
|
const prov = providersById.get(r.providerId);
|
|
@@ -5206,14 +6360,15 @@ Error: ${launchPlan.error}
|
|
|
5206
6360
|
supportedParameters: route.supportedParameters,
|
|
5207
6361
|
reasoning: route.reasoning,
|
|
5208
6362
|
interleavedReasoningField: route.interleavedReasoningField,
|
|
5209
|
-
headers: route.headers
|
|
6363
|
+
headers: route.headers,
|
|
6364
|
+
refreshToken: route.refreshToken
|
|
5210
6365
|
}], { debug: trace });
|
|
5211
6366
|
proxyPort = proxyHandle.port;
|
|
5212
6367
|
}
|
|
5213
6368
|
const startingFavorite = resolvedFavorites.find(
|
|
5214
6369
|
(r) => r.providerId === activeProvider.id && r.model.id === selectedModel.id
|
|
5215
6370
|
) ?? resolvedFavorites[0];
|
|
5216
|
-
const { profilePath, catalogPath } = favoritesActive && resolvedFavorites.length > 0 && proxyPort && startingFavorite ? await writeFavoritesLaunchArtifacts(resolvedFavorites, startingFavorite, proxyPort) : await writeLaunchArtifacts(route, selectedModel, activeProvider.name, proxyPort);
|
|
6371
|
+
const { profilePath, catalogPath } = mixedPlan && proxyPort ? await writeMixedLaunchArtifacts(mixedPlan, proxyPort) : favoritesActive && resolvedFavorites.length > 0 && proxyPort && startingFavorite ? await writeFavoritesLaunchArtifacts(resolvedFavorites, startingFavorite, proxyPort) : await writeLaunchArtifacts(route, selectedModel, activeProvider.name, proxyPort);
|
|
5217
6372
|
writeSessionLock({
|
|
5218
6373
|
pid: process.pid,
|
|
5219
6374
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -5227,7 +6382,11 @@ Error: ${launchPlan.error}
|
|
|
5227
6382
|
console.log("");
|
|
5228
6383
|
console.log(pc7.bold(pc7.cyan(" CONFIG PREVIEW \u2014 relay-ai codex")));
|
|
5229
6384
|
console.log("");
|
|
5230
|
-
if (
|
|
6385
|
+
if (mixedPlan) {
|
|
6386
|
+
console.log(` ${pc7.bold("Mode:")} Native + Relay mixed catalog`);
|
|
6387
|
+
console.log(` ${pc7.bold("Native:")} ${mixedPlan.nativeModelIds.size} native Codex models`);
|
|
6388
|
+
console.log(` ${pc7.bold("Relay:")} ${mixedPlan.relayRoutes.length} Relay routes (${mixedPlan.subagentModelCount} Codex SubAgent model)`);
|
|
6389
|
+
} else if (favoritesActive && resolvedFavorites.length > 0) {
|
|
5231
6390
|
console.log(` ${pc7.bold("Mode:")} Favorites Catalog (${resolvedFavorites.length} model${resolvedFavorites.length !== 1 ? "s" : ""})`);
|
|
5232
6391
|
console.log("");
|
|
5233
6392
|
console.log(` ${pc7.bold("Models:")}`);
|
|
@@ -5258,7 +6417,7 @@ Error: ${launchPlan.error}
|
|
|
5258
6417
|
}
|
|
5259
6418
|
}
|
|
5260
6419
|
const favoritesLaunch = favoritesActive && resolvedFavorites.length > 0;
|
|
5261
|
-
const launchModelId = favoritesLaunch ? codexCliFavoritesSlug(activeProvider.id, selectedModel.id) : selectedModel.id;
|
|
6420
|
+
const launchModelId = favoritesLaunch ? codexCliFavoritesSlug(activeProvider.id, selectedModel.id) : mixedPlan?.selectedSlug ?? selectedModel.id;
|
|
5262
6421
|
if (!agentStdout) {
|
|
5263
6422
|
logActiveModel(modelLabel, launchModelId);
|
|
5264
6423
|
printCodexCliCleanupPanel("relay-ai codex --restore");
|
|
@@ -5274,9 +6433,10 @@ Error: ${launchPlan.error}
|
|
|
5274
6433
|
};
|
|
5275
6434
|
const childEnv = buildCodexChildEnv(
|
|
5276
6435
|
favoritesLaunch || route.tier === "cloud-code" ? dummyRoute : route,
|
|
5277
|
-
proxyPort
|
|
6436
|
+
proxyPort,
|
|
6437
|
+
{ mixedNative: !!mixedPlan }
|
|
5278
6438
|
);
|
|
5279
|
-
const hadProxy = (route.tier === "proxy" || route.tier === "cloud-code" || favoritesLaunch) && !!proxyPort;
|
|
6439
|
+
const hadProxy = (!!mixedPlan || route.tier === "proxy" || route.tier === "cloud-code" || favoritesLaunch) && !!proxyPort;
|
|
5280
6440
|
const exitCode = await launchCodex(launchModelId, childEnv, passthroughArgs);
|
|
5281
6441
|
if (trace) printTraceLog(debugLogPath);
|
|
5282
6442
|
printCodexCleanupReminder(hadProxy);
|
|
@@ -5301,15 +6461,15 @@ import * as p10 from "@clack/prompts";
|
|
|
5301
6461
|
import { spawn as spawn3 } from "child_process";
|
|
5302
6462
|
import { existsSync as existsSync5, mkdirSync as mkdirSync2, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5303
6463
|
import { homedir as homedir5, tmpdir } from "os";
|
|
5304
|
-
import { join as
|
|
6464
|
+
import { join as join6 } from "path";
|
|
5305
6465
|
var isWindows3 = process.platform === "win32";
|
|
5306
6466
|
var GEMINI_API_KEY_AUTH_TYPE = "gemini-api-key";
|
|
5307
6467
|
var GEMINI_FALLBACK_PATHS = isWindows3 ? [
|
|
5308
|
-
|
|
5309
|
-
|
|
6468
|
+
join6(process.env["APPDATA"] ?? homedir5(), "npm", "gemini.cmd"),
|
|
6469
|
+
join6(process.env["APPDATA"] ?? homedir5(), "npm", "gemini")
|
|
5310
6470
|
] : [
|
|
5311
|
-
|
|
5312
|
-
|
|
6471
|
+
join6(homedir5(), ".local", "bin", "gemini"),
|
|
6472
|
+
join6(homedir5(), ".npm", "bin", "gemini"),
|
|
5313
6473
|
"/usr/local/bin/gemini",
|
|
5314
6474
|
"/opt/homebrew/bin/gemini"
|
|
5315
6475
|
];
|
|
@@ -5330,7 +6490,7 @@ function buildGeminiChildEnv(proxyPort, proxyToken) {
|
|
|
5330
6490
|
return env;
|
|
5331
6491
|
}
|
|
5332
6492
|
function createGeminiCliHomeOverlay() {
|
|
5333
|
-
const cliHome = mkdtempSync(
|
|
6493
|
+
const cliHome = mkdtempSync(join6(tmpdir(), "relay-ai-gemini-"));
|
|
5334
6494
|
const settings = {
|
|
5335
6495
|
security: {
|
|
5336
6496
|
auth: {
|
|
@@ -5338,9 +6498,9 @@ function createGeminiCliHomeOverlay() {
|
|
|
5338
6498
|
}
|
|
5339
6499
|
}
|
|
5340
6500
|
};
|
|
5341
|
-
const geminiDir =
|
|
6501
|
+
const geminiDir = join6(cliHome, ".gemini");
|
|
5342
6502
|
mkdirSync2(geminiDir);
|
|
5343
|
-
writeFileSync2(
|
|
6503
|
+
writeFileSync2(join6(geminiDir, "settings.json"), `${JSON.stringify(settings, null, 2)}
|
|
5344
6504
|
`, {
|
|
5345
6505
|
encoding: "utf8",
|
|
5346
6506
|
mode: 384
|
|
@@ -5557,8 +6717,8 @@ function mergeConsecutiveMessages2(messages) {
|
|
|
5557
6717
|
}
|
|
5558
6718
|
return merged;
|
|
5559
6719
|
}
|
|
5560
|
-
function stripGeminiIdentity(
|
|
5561
|
-
return
|
|
6720
|
+
function stripGeminiIdentity(text5) {
|
|
6721
|
+
return text5.replace(/You are Gemini CLI[\s\S]*?(?=\n\n|$)/gi, "").replace(/I'm Gemini CLI[\s\S]*?(?=\n\n|$)/gi, "").replace(/Gemini CLI/gi, "AI CLI");
|
|
5562
6722
|
}
|
|
5563
6723
|
function translateGeminiRequest(body, options = {}) {
|
|
5564
6724
|
let system;
|
|
@@ -5576,16 +6736,16 @@ function translateGeminiRequest(body, options = {}) {
|
|
|
5576
6736
|
const turnParts = turn.parts || [];
|
|
5577
6737
|
for (const p15 of turnParts) {
|
|
5578
6738
|
if (p15.text !== void 0) {
|
|
5579
|
-
const
|
|
5580
|
-
if (
|
|
5581
|
-
const tokens =
|
|
6739
|
+
const text5 = stripGeminiIdentity(p15.text);
|
|
6740
|
+
if (text5.includes("<thinking>")) {
|
|
6741
|
+
const tokens = text5.split(/<thinking>([\s\S]*?)<\/thinking>/);
|
|
5582
6742
|
for (let i = 0; i < tokens.length; i++) {
|
|
5583
6743
|
const token = tokens[i].trim();
|
|
5584
6744
|
if (!token) continue;
|
|
5585
6745
|
parts.push({ type: i % 2 === 1 ? "reasoning" : "text", text: token });
|
|
5586
6746
|
}
|
|
5587
6747
|
} else {
|
|
5588
|
-
parts.push({ type: "text", text:
|
|
6748
|
+
parts.push({ type: "text", text: text5 });
|
|
5589
6749
|
}
|
|
5590
6750
|
} else if (p15.inlineData) {
|
|
5591
6751
|
parts.push({
|
|
@@ -5700,7 +6860,11 @@ async function startGeminiProxy(routes, debug = false) {
|
|
|
5700
6860
|
authType: route.authType,
|
|
5701
6861
|
oauthAccountId: route.oauthAccountId,
|
|
5702
6862
|
providerData: route.providerData,
|
|
5703
|
-
headers: route.headers
|
|
6863
|
+
headers: route.headers,
|
|
6864
|
+
refreshToken: route.refreshToken,
|
|
6865
|
+
onTokenRefreshed: (refreshed) => {
|
|
6866
|
+
route.apiKey = refreshed;
|
|
6867
|
+
}
|
|
5704
6868
|
});
|
|
5705
6869
|
models.set(route.aliasId, m);
|
|
5706
6870
|
}
|
|
@@ -5766,14 +6930,14 @@ ${rawBody}`);
|
|
|
5766
6930
|
const current = sessionRouteOverride ?? (lookupGeminiRoute(routes, requestedModel) ?? defaultRoute);
|
|
5767
6931
|
const availableList = routes.map((r) => ` - ${r.aliasId} (${r.displayName})`).join("\n");
|
|
5768
6932
|
const exampleId = routes.length > 1 ? routes[1].aliasId : routes[0]?.aliasId ?? "deepseek-v4";
|
|
5769
|
-
const
|
|
6933
|
+
const text5 = `Current model: ${current.displayName} (${current.aliasId})
|
|
5770
6934
|
|
|
5771
6935
|
Available models:
|
|
5772
6936
|
${availableList}
|
|
5773
6937
|
|
|
5774
6938
|
\u{1F4A1} To switch models, type: .model <id>
|
|
5775
6939
|
Example: .model ${exampleId}`;
|
|
5776
|
-
sendMockGeminiResponse(res,
|
|
6940
|
+
sendMockGeminiResponse(res, text5, isStream, current.aliasId);
|
|
5777
6941
|
return;
|
|
5778
6942
|
}
|
|
5779
6943
|
const targetRoute = lookupGeminiRoute(routes, modelCommand);
|
|
@@ -5836,33 +7000,33 @@ ${JSON.stringify(params, null, 2)}`);
|
|
|
5836
7000
|
`);
|
|
5837
7001
|
}
|
|
5838
7002
|
if (p15.type === "reasoning") {
|
|
5839
|
-
let
|
|
7003
|
+
let text5 = p15.textDelta ?? p15.text ?? "";
|
|
5840
7004
|
if (!isThinking) {
|
|
5841
7005
|
isThinking = true;
|
|
5842
|
-
|
|
5843
|
-
` +
|
|
7006
|
+
text5 = `<thinking>
|
|
7007
|
+
` + text5;
|
|
5844
7008
|
}
|
|
5845
7009
|
const chunk = {
|
|
5846
|
-
candidates: [{ content: { role: "model", parts: [{ text:
|
|
7010
|
+
candidates: [{ content: { role: "model", parts: [{ text: text5 }] } }],
|
|
5847
7011
|
modelVersion: route.aliasId
|
|
5848
7012
|
};
|
|
5849
7013
|
res.write(`data: ${JSON.stringify(chunk)}
|
|
5850
7014
|
|
|
5851
7015
|
`);
|
|
5852
7016
|
} else if (p15.type === "text-delta") {
|
|
5853
|
-
let
|
|
7017
|
+
let text5 = p15.textDelta ?? p15.text ?? "";
|
|
5854
7018
|
if (isThinking) {
|
|
5855
7019
|
isThinking = false;
|
|
5856
|
-
|
|
7020
|
+
text5 = `
|
|
5857
7021
|
</thinking>
|
|
5858
7022
|
|
|
5859
|
-
` +
|
|
7023
|
+
` + text5;
|
|
5860
7024
|
}
|
|
5861
7025
|
const chunk = {
|
|
5862
7026
|
candidates: [{
|
|
5863
7027
|
content: {
|
|
5864
7028
|
role: "model",
|
|
5865
|
-
parts: [{ text:
|
|
7029
|
+
parts: [{ text: text5 }]
|
|
5866
7030
|
}
|
|
5867
7031
|
}],
|
|
5868
7032
|
modelVersion: route.aliasId
|
|
@@ -6045,28 +7209,28 @@ function parseModelCommand(turn) {
|
|
|
6045
7209
|
if (!turn || turn.role !== "user") return null;
|
|
6046
7210
|
const parts = turn.parts || [];
|
|
6047
7211
|
if (parts.length !== 1) return null;
|
|
6048
|
-
const
|
|
6049
|
-
if (typeof
|
|
6050
|
-
const trimmed =
|
|
7212
|
+
const text5 = parts[0]?.text;
|
|
7213
|
+
if (typeof text5 !== "string") return null;
|
|
7214
|
+
const trimmed = text5.trim();
|
|
6051
7215
|
if (!trimmed.startsWith(".model")) return null;
|
|
6052
7216
|
if (trimmed === ".model") return "";
|
|
6053
7217
|
if (trimmed.charAt(6) !== " ") return null;
|
|
6054
7218
|
return trimmed.slice(7).trim();
|
|
6055
7219
|
}
|
|
6056
|
-
function sendMockGeminiResponse(res,
|
|
7220
|
+
function sendMockGeminiResponse(res, text5, isStream, modelVersion) {
|
|
6057
7221
|
if (isStream) {
|
|
6058
7222
|
res.writeHead(200, {
|
|
6059
7223
|
"Content-Type": "text/event-stream",
|
|
6060
7224
|
"Cache-Control": "no-cache",
|
|
6061
7225
|
"Connection": "keep-alive"
|
|
6062
7226
|
});
|
|
6063
|
-
writeGeminiStreamText(res,
|
|
7227
|
+
writeGeminiStreamText(res, text5, modelVersion);
|
|
6064
7228
|
res.end();
|
|
6065
7229
|
} else {
|
|
6066
7230
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6067
7231
|
res.end(JSON.stringify({
|
|
6068
7232
|
candidates: [{
|
|
6069
|
-
content: { role: "model", parts: [{ text:
|
|
7233
|
+
content: { role: "model", parts: [{ text: text5 }] },
|
|
6070
7234
|
finishReason: "STOP"
|
|
6071
7235
|
}],
|
|
6072
7236
|
usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 },
|
|
@@ -6074,10 +7238,10 @@ function sendMockGeminiResponse(res, text4, isStream, modelVersion) {
|
|
|
6074
7238
|
}));
|
|
6075
7239
|
}
|
|
6076
7240
|
}
|
|
6077
|
-
function writeGeminiStreamText(res,
|
|
7241
|
+
function writeGeminiStreamText(res, text5, modelVersion) {
|
|
6078
7242
|
const chunk = {
|
|
6079
7243
|
candidates: [{
|
|
6080
|
-
content: { role: "model", parts: [{ text:
|
|
7244
|
+
content: { role: "model", parts: [{ text: text5 }] },
|
|
6081
7245
|
finishReason: "STOP"
|
|
6082
7246
|
}],
|
|
6083
7247
|
usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 },
|
|
@@ -7471,6 +8635,26 @@ var MAX_REASONING_ECHOES_PER_CONVERSATION = 20;
|
|
|
7471
8635
|
function isCloudCodeOAuthRoute(route) {
|
|
7472
8636
|
return route.providerId === "antigravity" && route.authType === "oauth" && route.modelFormat === "cloud-code";
|
|
7473
8637
|
}
|
|
8638
|
+
async function createRouteLanguageModel(route) {
|
|
8639
|
+
const spec = {
|
|
8640
|
+
npm: route.npm,
|
|
8641
|
+
modelId: route.upstreamModelId,
|
|
8642
|
+
apiKey: route.apiKey,
|
|
8643
|
+
baseURL: route.baseURL,
|
|
8644
|
+
providerId: route.providerId,
|
|
8645
|
+
authType: route.authType,
|
|
8646
|
+
oauthAccountId: route.oauthAccountId,
|
|
8647
|
+
providerData: route.providerData
|
|
8648
|
+
};
|
|
8649
|
+
if (route.headers !== void 0) spec.headers = route.headers;
|
|
8650
|
+
if (route.refreshToken) {
|
|
8651
|
+
spec.refreshToken = route.refreshToken;
|
|
8652
|
+
spec.onTokenRefreshed = (refreshed) => {
|
|
8653
|
+
route.apiKey = refreshed;
|
|
8654
|
+
};
|
|
8655
|
+
}
|
|
8656
|
+
return createLanguageModel(spec);
|
|
8657
|
+
}
|
|
7474
8658
|
function isUserTurnRequest(parsed) {
|
|
7475
8659
|
return typeof parsed?.requestId === "string" && parsed.requestId.startsWith("agent/");
|
|
7476
8660
|
}
|
|
@@ -7874,11 +9058,11 @@ async function handleCloudCodeForwardRequest(res, route, parsed, lowerUrl, log14
|
|
|
7874
9058
|
});
|
|
7875
9059
|
res.end(body);
|
|
7876
9060
|
}
|
|
7877
|
-
function emitThinkingDelta(res, route, responseId,
|
|
7878
|
-
if (!
|
|
9061
|
+
function emitThinkingDelta(res, route, responseId, text5, startSse) {
|
|
9062
|
+
if (!text5) return;
|
|
7879
9063
|
startSse();
|
|
7880
9064
|
const chunk = formatCloudCodeChunk({
|
|
7881
|
-
thought:
|
|
9065
|
+
thought: text5,
|
|
7882
9066
|
modelVersion: route.catalogId,
|
|
7883
9067
|
responseId
|
|
7884
9068
|
});
|
|
@@ -7886,9 +9070,9 @@ function emitThinkingDelta(res, route, responseId, text4, startSse) {
|
|
|
7886
9070
|
|
|
7887
9071
|
`);
|
|
7888
9072
|
}
|
|
7889
|
-
function trailingPartial(
|
|
7890
|
-
for (let len = Math.min(tag.length - 1,
|
|
7891
|
-
if (
|
|
9073
|
+
function trailingPartial(text5, tag) {
|
|
9074
|
+
for (let len = Math.min(tag.length - 1, text5.length); len > 0; len--) {
|
|
9075
|
+
if (text5.endsWith(tag.slice(0, len))) return len;
|
|
7892
9076
|
}
|
|
7893
9077
|
return 0;
|
|
7894
9078
|
}
|
|
@@ -7900,13 +9084,13 @@ function createThinkFilter() {
|
|
|
7900
9084
|
let src = partial + chunk;
|
|
7901
9085
|
partial = "";
|
|
7902
9086
|
let thought = "";
|
|
7903
|
-
let
|
|
9087
|
+
let text5 = "";
|
|
7904
9088
|
while (src.length > 0) {
|
|
7905
9089
|
if (state === "scanning") {
|
|
7906
9090
|
const idx = src.indexOf("<think>");
|
|
7907
9091
|
if (idx === -1) {
|
|
7908
9092
|
const len = trailingPartial(src, "<think>");
|
|
7909
|
-
|
|
9093
|
+
text5 += src.slice(0, src.length - len);
|
|
7910
9094
|
if (len > 0) {
|
|
7911
9095
|
partial = src.slice(src.length - len);
|
|
7912
9096
|
} else {
|
|
@@ -7914,7 +9098,7 @@ function createThinkFilter() {
|
|
|
7914
9098
|
}
|
|
7915
9099
|
break;
|
|
7916
9100
|
}
|
|
7917
|
-
|
|
9101
|
+
text5 += src.slice(0, idx);
|
|
7918
9102
|
src = src.slice(idx + 7);
|
|
7919
9103
|
state = "inside";
|
|
7920
9104
|
} else {
|
|
@@ -7931,7 +9115,7 @@ function createThinkFilter() {
|
|
|
7931
9115
|
state = "passthrough";
|
|
7932
9116
|
}
|
|
7933
9117
|
}
|
|
7934
|
-
return { thought, text:
|
|
9118
|
+
return { thought, text: text5 };
|
|
7935
9119
|
};
|
|
7936
9120
|
}
|
|
7937
9121
|
function emitStreamError(res, route, responseId, message, startSse) {
|
|
@@ -7993,8 +9177,8 @@ function respondUnsupportedMedia(res, route, streaming) {
|
|
|
7993
9177
|
metadata: {}
|
|
7994
9178
|
});
|
|
7995
9179
|
}
|
|
7996
|
-
function parsePseudoToolCall(
|
|
7997
|
-
const trimmed =
|
|
9180
|
+
function parsePseudoToolCall(text5, knownToolNames) {
|
|
9181
|
+
const trimmed = text5.trim();
|
|
7998
9182
|
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
7999
9183
|
try {
|
|
8000
9184
|
const obj = JSON.parse(trimmed);
|
|
@@ -8021,16 +9205,7 @@ async function handleStreamingRequest(res, route, providerOptions, parsed, log14
|
|
|
8021
9205
|
providerOptions,
|
|
8022
9206
|
sdkParams.providerOptions
|
|
8023
9207
|
);
|
|
8024
|
-
const langModel = await
|
|
8025
|
-
npm: route.npm,
|
|
8026
|
-
modelId: route.upstreamModelId,
|
|
8027
|
-
apiKey: route.apiKey,
|
|
8028
|
-
baseURL: route.baseURL,
|
|
8029
|
-
providerId: route.providerId,
|
|
8030
|
-
authType: route.authType,
|
|
8031
|
-
oauthAccountId: route.oauthAccountId,
|
|
8032
|
-
providerData: route.providerData
|
|
8033
|
-
});
|
|
9208
|
+
const langModel = await createRouteLanguageModel(route);
|
|
8034
9209
|
const responseId = `relay-${Date.now()}`;
|
|
8035
9210
|
const { fullStream } = streamText3({
|
|
8036
9211
|
model: langModel,
|
|
@@ -8092,18 +9267,18 @@ async function handleStreamingRequest(res, route, providerOptions, parsed, log14
|
|
|
8092
9267
|
continue;
|
|
8093
9268
|
}
|
|
8094
9269
|
if (p15.type === "text-delta") {
|
|
8095
|
-
const { thought, text:
|
|
9270
|
+
const { thought, text: text5 } = thinkFilter(reasoningDeltaText(p15));
|
|
8096
9271
|
if (thought) {
|
|
8097
9272
|
responseReasoning += thought;
|
|
8098
9273
|
emitThinkingDelta(res, route, responseId, thought, startSse);
|
|
8099
9274
|
}
|
|
8100
|
-
if (
|
|
8101
|
-
log14(`[gateway] text-delta: ${JSON.stringify(
|
|
8102
|
-
if (!bufferingJsonText && (textBuffer +
|
|
9275
|
+
if (text5) {
|
|
9276
|
+
log14(`[gateway] text-delta: ${JSON.stringify(text5.slice(0, 500))}`);
|
|
9277
|
+
if (!bufferingJsonText && (textBuffer + text5).trimStart().startsWith("{")) {
|
|
8103
9278
|
bufferingJsonText = true;
|
|
8104
9279
|
}
|
|
8105
9280
|
if (bufferingJsonText) {
|
|
8106
|
-
textBuffer +=
|
|
9281
|
+
textBuffer += text5;
|
|
8107
9282
|
const pseudoTool = textBuffer.trimEnd().endsWith("}") ? parsePseudoToolCall(textBuffer, knownToolNames) : null;
|
|
8108
9283
|
if (pseudoTool) {
|
|
8109
9284
|
log14(`[gateway] parsed pseudo tool-call from text: ${pseudoTool.name}`);
|
|
@@ -8112,7 +9287,7 @@ async function handleStreamingRequest(res, route, providerOptions, parsed, log14
|
|
|
8112
9287
|
} else {
|
|
8113
9288
|
startSse();
|
|
8114
9289
|
const chunk = formatCloudCodeChunk({
|
|
8115
|
-
text:
|
|
9290
|
+
text: text5,
|
|
8116
9291
|
modelVersion: route.catalogId,
|
|
8117
9292
|
responseId
|
|
8118
9293
|
});
|
|
@@ -8207,16 +9382,7 @@ async function handleUnaryRequest(res, route, providerOptions, parsed, log14, op
|
|
|
8207
9382
|
providerOptions,
|
|
8208
9383
|
sdkParams.providerOptions
|
|
8209
9384
|
);
|
|
8210
|
-
const langModel = await
|
|
8211
|
-
npm: route.npm,
|
|
8212
|
-
modelId: route.upstreamModelId,
|
|
8213
|
-
apiKey: route.apiKey,
|
|
8214
|
-
baseURL: route.baseURL,
|
|
8215
|
-
providerId: route.providerId,
|
|
8216
|
-
authType: route.authType,
|
|
8217
|
-
oauthAccountId: route.oauthAccountId,
|
|
8218
|
-
providerData: route.providerData
|
|
8219
|
-
});
|
|
9385
|
+
const langModel = await createRouteLanguageModel(route);
|
|
8220
9386
|
const responseId = `relay-${Date.now()}`;
|
|
8221
9387
|
const result = await generateText3({
|
|
8222
9388
|
model: langModel,
|
|
@@ -8275,7 +9441,9 @@ async function resolveAntigravityLaunchRoutes(opts) {
|
|
|
8275
9441
|
apiKey,
|
|
8276
9442
|
authType: opts.provider.authType,
|
|
8277
9443
|
oauthAccountId: opts.provider.oauthAccountId,
|
|
8278
|
-
providerData: opts.provider.providerData
|
|
9444
|
+
providerData: opts.provider.providerData,
|
|
9445
|
+
headers: opts.provider.headers,
|
|
9446
|
+
refreshToken: providerRefreshToken(opts.provider.id, opts.provider.authType, opts.provider.authRef)
|
|
8279
9447
|
};
|
|
8280
9448
|
const ctx = {
|
|
8281
9449
|
agent: "antigravity",
|
|
@@ -8309,19 +9477,19 @@ async function resolveAntigravityLaunchRoutes(opts) {
|
|
|
8309
9477
|
}
|
|
8310
9478
|
|
|
8311
9479
|
// src/antigravity/launch-cli.ts
|
|
8312
|
-
import { execFileSync as
|
|
9480
|
+
import { execFileSync as execFileSync3, execSync as execSync3 } from "child_process";
|
|
8313
9481
|
import spawn4 from "cross-spawn";
|
|
8314
9482
|
import { existsSync as existsSync6 } from "fs";
|
|
8315
9483
|
import { homedir as homedir6 } from "os";
|
|
8316
|
-
import { join as
|
|
9484
|
+
import { join as join7 } from "path";
|
|
8317
9485
|
var isWindows4 = process.platform === "win32";
|
|
8318
9486
|
var FALLBACK_PATHS = isWindows4 ? [
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
9487
|
+
join7(process.env["APPDATA"] ?? homedir6(), "npm", "agy.cmd"),
|
|
9488
|
+
join7(process.env["APPDATA"] ?? homedir6(), "npm", "agy"),
|
|
9489
|
+
join7(homedir6(), "AppData", "Roaming", "npm", "agy.cmd")
|
|
8322
9490
|
] : [
|
|
8323
|
-
|
|
8324
|
-
|
|
9491
|
+
join7(homedir6(), ".local", "bin", "agy"),
|
|
9492
|
+
join7(homedir6(), ".npm", "bin", "agy"),
|
|
8325
9493
|
"/usr/local/bin/agy",
|
|
8326
9494
|
"/opt/homebrew/bin/agy"
|
|
8327
9495
|
];
|
|
@@ -8347,7 +9515,7 @@ function readAntigravityCliVersion(binaryPath = findAntigravityCliBinary() ?? vo
|
|
|
8347
9515
|
return { version: null, error: 'Antigravity CLI binary "agy" not found' };
|
|
8348
9516
|
}
|
|
8349
9517
|
try {
|
|
8350
|
-
const raw =
|
|
9518
|
+
const raw = execFileSync3(binaryPath, ["--version"], {
|
|
8351
9519
|
encoding: "utf8",
|
|
8352
9520
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8353
9521
|
}).trim();
|
|
@@ -8396,10 +9564,10 @@ function launchAntigravityCli(env, extraArgs) {
|
|
|
8396
9564
|
}
|
|
8397
9565
|
|
|
8398
9566
|
// src/antigravity/launch-ide.ts
|
|
8399
|
-
import { execFileSync as
|
|
9567
|
+
import { execFileSync as execFileSync4, execSync as execSync4, spawn as spawn5 } from "child_process";
|
|
8400
9568
|
import { existsSync as existsSync7 } from "fs";
|
|
8401
9569
|
import { homedir as homedir7 } from "os";
|
|
8402
|
-
import { join as
|
|
9570
|
+
import { join as join8 } from "path";
|
|
8403
9571
|
|
|
8404
9572
|
// src/antigravity/ide-profile.ts
|
|
8405
9573
|
import fs from "fs";
|
|
@@ -8433,8 +9601,8 @@ function prepareIdeProfile(profileDir, gatewayUrl) {
|
|
|
8433
9601
|
}
|
|
8434
9602
|
|
|
8435
9603
|
// src/antigravity/launch-ide.ts
|
|
8436
|
-
var LINUX_APP_PROFILE_DIR =
|
|
8437
|
-
var LINUX_IDE_PROFILE_DIR =
|
|
9604
|
+
var LINUX_APP_PROFILE_DIR = join8(homedir7(), ".relay-ai", "antigravity", "app-profile");
|
|
9605
|
+
var LINUX_IDE_PROFILE_DIR = join8(homedir7(), ".relay-ai", "antigravity", "profile");
|
|
8438
9606
|
function sleep(ms) {
|
|
8439
9607
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
8440
9608
|
}
|
|
@@ -8442,7 +9610,7 @@ function linuxAntigravityBinary() {
|
|
|
8442
9610
|
const candidates = [
|
|
8443
9611
|
"/usr/share/antigravity/antigravity",
|
|
8444
9612
|
"/opt/antigravity/antigravity",
|
|
8445
|
-
|
|
9613
|
+
join8(homedir7(), ".local", "share", "antigravity", "antigravity")
|
|
8446
9614
|
];
|
|
8447
9615
|
for (const candidate of candidates) {
|
|
8448
9616
|
if (existsSync7(candidate)) return candidate;
|
|
@@ -8500,7 +9668,7 @@ function defaultProcessList() {
|
|
|
8500
9668
|
const psArgs = process.platform === "linux" ? ["-eo", "pid=,args="] : ["-axo", "pid=,command="];
|
|
8501
9669
|
if (process.platform !== "darwin" && process.platform !== "linux") return "";
|
|
8502
9670
|
try {
|
|
8503
|
-
return
|
|
9671
|
+
return execFileSync4("ps", psArgs, {
|
|
8504
9672
|
encoding: "utf8",
|
|
8505
9673
|
stdio: ["ignore", "pipe", "ignore"],
|
|
8506
9674
|
maxBuffer: 1024 * 1024 * 4
|
|
@@ -8564,11 +9732,11 @@ function quitAntigravityIdeGracefully() {
|
|
|
8564
9732
|
}
|
|
8565
9733
|
if (process.platform !== "darwin") return;
|
|
8566
9734
|
try {
|
|
8567
|
-
|
|
9735
|
+
execFileSync4("osascript", ["-e", 'tell application "Antigravity IDE" to quit'], {
|
|
8568
9736
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8569
9737
|
});
|
|
8570
9738
|
} catch {
|
|
8571
|
-
|
|
9739
|
+
execFileSync4("osascript", ["-e", 'tell application id "com.google.antigravity-ide" to quit'], {
|
|
8572
9740
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8573
9741
|
});
|
|
8574
9742
|
}
|
|
@@ -8584,11 +9752,11 @@ function quitAntigravityAppGracefully() {
|
|
|
8584
9752
|
}
|
|
8585
9753
|
if (process.platform !== "darwin") return;
|
|
8586
9754
|
try {
|
|
8587
|
-
|
|
9755
|
+
execFileSync4("osascript", ["-e", 'tell application "Antigravity" to quit'], {
|
|
8588
9756
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8589
9757
|
});
|
|
8590
9758
|
} catch {
|
|
8591
|
-
|
|
9759
|
+
execFileSync4("osascript", ["-e", 'tell application id "com.google.antigravity" to quit'], {
|
|
8592
9760
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8593
9761
|
});
|
|
8594
9762
|
}
|
|
@@ -8597,15 +9765,15 @@ function findAntigravityAppBinary() {
|
|
|
8597
9765
|
const override = getAppPathOverride("antigravity");
|
|
8598
9766
|
if (override) return existsSync7(override) ? override : null;
|
|
8599
9767
|
if (process.platform === "win32") {
|
|
8600
|
-
const localAppData = process.env["LOCALAPPDATA"] ??
|
|
8601
|
-
const winPath =
|
|
9768
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? join8(homedir7(), "AppData", "Local");
|
|
9769
|
+
const winPath = join8(localAppData, "Programs", "Antigravity", "Antigravity.exe");
|
|
8602
9770
|
return existsSync7(winPath) ? winPath : null;
|
|
8603
9771
|
}
|
|
8604
9772
|
if (process.platform === "linux") return linuxAntigravityBinary();
|
|
8605
9773
|
if (process.platform !== "darwin") return null;
|
|
8606
9774
|
const defaultPath = "/Applications/Antigravity.app/Contents/MacOS/Antigravity";
|
|
8607
9775
|
if (existsSync7(defaultPath)) return defaultPath;
|
|
8608
|
-
const homePath =
|
|
9776
|
+
const homePath = join8(homedir7(), "Applications", "Antigravity.app", "Contents", "MacOS", "Antigravity");
|
|
8609
9777
|
if (existsSync7(homePath)) return homePath;
|
|
8610
9778
|
return null;
|
|
8611
9779
|
}
|
|
@@ -8613,15 +9781,15 @@ function findAntigravityIdeBinary() {
|
|
|
8613
9781
|
const override = getAppPathOverride("antigravity-ide");
|
|
8614
9782
|
if (override) return existsSync7(override) ? override : null;
|
|
8615
9783
|
if (process.platform === "win32") {
|
|
8616
|
-
const localAppData = process.env["LOCALAPPDATA"] ??
|
|
8617
|
-
const winPath =
|
|
9784
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? join8(homedir7(), "AppData", "Local");
|
|
9785
|
+
const winPath = join8(localAppData, "Programs", "Antigravity IDE", "Antigravity IDE.exe");
|
|
8618
9786
|
return existsSync7(winPath) ? winPath : null;
|
|
8619
9787
|
}
|
|
8620
9788
|
if (process.platform === "linux") return linuxAntigravityBinary();
|
|
8621
9789
|
if (process.platform !== "darwin") return null;
|
|
8622
9790
|
const defaultPath = "/Applications/Antigravity IDE.app/Contents/Resources/app/bin/antigravity-ide";
|
|
8623
9791
|
if (existsSync7(defaultPath)) return defaultPath;
|
|
8624
|
-
const homePath =
|
|
9792
|
+
const homePath = join8(homedir7(), "Applications", "Antigravity IDE.app", "Contents", "Resources", "app", "bin", "antigravity-ide");
|
|
8625
9793
|
if (existsSync7(homePath)) return homePath;
|
|
8626
9794
|
return null;
|
|
8627
9795
|
}
|
|
@@ -8682,7 +9850,7 @@ function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
|
|
|
8682
9850
|
return;
|
|
8683
9851
|
}
|
|
8684
9852
|
prepareIdeProfile(profileDir, gatewayUrl);
|
|
8685
|
-
const relayExtensionsDir =
|
|
9853
|
+
const relayExtensionsDir = join8(homedir7(), ".relay-ai", "antigravity", "extensions");
|
|
8686
9854
|
const args = [
|
|
8687
9855
|
`--user-data-dir=${profileDir}`,
|
|
8688
9856
|
`--extensions-dir=${relayExtensionsDir}`,
|
|
@@ -8712,7 +9880,7 @@ function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
|
|
|
8712
9880
|
|
|
8713
9881
|
// src/antigravity.ts
|
|
8714
9882
|
import { homedir as homedir8 } from "os";
|
|
8715
|
-
import { join as
|
|
9883
|
+
import { join as join9 } from "path";
|
|
8716
9884
|
var SHUTDOWN_DRAIN_MS = 500;
|
|
8717
9885
|
var AGY_FAVORITES_PROVIDER_ID = "__relay_agy_favorites__";
|
|
8718
9886
|
var AGY_FAVORITES_PROVIDER_LABEL = "\u2605 Antigravity CLI Favorites";
|
|
@@ -9006,7 +10174,7 @@ async function runAntigravityAppCommand(childArgs, trace = false, boot) {
|
|
|
9006
10174
|
trace,
|
|
9007
10175
|
boot,
|
|
9008
10176
|
async (env, _routes, gatewayHandle) => {
|
|
9009
|
-
const profileDir =
|
|
10177
|
+
const profileDir = join9(homedir8(), ".relay-ai", "antigravity", "app-profile");
|
|
9010
10178
|
if (isAntigravityAppRunning(profileDir)) {
|
|
9011
10179
|
const restart = await p11.confirm({
|
|
9012
10180
|
message: "Restart Antigravity to apply this Relay gateway?",
|
|
@@ -9054,7 +10222,7 @@ async function runAntigravityIdeCommand(childArgs, trace = false, boot) {
|
|
|
9054
10222
|
trace,
|
|
9055
10223
|
boot,
|
|
9056
10224
|
async (env, _routes, gatewayHandle) => {
|
|
9057
|
-
const profileDir =
|
|
10225
|
+
const profileDir = join9(homedir8(), ".relay-ai", "antigravity", "profile");
|
|
9058
10226
|
if (isAntigravityIdeRunning(profileDir)) {
|
|
9059
10227
|
const restart = await p11.confirm({
|
|
9060
10228
|
message: "Restart Antigravity IDE to apply this Relay gateway?",
|
|
@@ -9099,6 +10267,8 @@ async function runAntigravityIdeCommand(childArgs, trace = false, boot) {
|
|
|
9099
10267
|
// src/codex-app.ts
|
|
9100
10268
|
import pc10 from "picocolors";
|
|
9101
10269
|
import * as p12 from "@clack/prompts";
|
|
10270
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
10271
|
+
import { join as join12 } from "path";
|
|
9102
10272
|
|
|
9103
10273
|
// src/codex/app-provider-routes.ts
|
|
9104
10274
|
function codexRouteToProxyRoute(provider, model, apiKey) {
|
|
@@ -9117,7 +10287,8 @@ function codexRouteToProxyRoute(provider, model, apiKey) {
|
|
|
9117
10287
|
supportedParameters: route.supportedParameters,
|
|
9118
10288
|
reasoning: route.reasoning,
|
|
9119
10289
|
interleavedReasoningField: route.interleavedReasoningField,
|
|
9120
|
-
headers: route.headers
|
|
10290
|
+
headers: route.headers,
|
|
10291
|
+
refreshToken: route.refreshToken
|
|
9121
10292
|
};
|
|
9122
10293
|
}
|
|
9123
10294
|
async function buildCodexAppProviderCatalogRoutes(provider, apiKey, selectedModelId, trace) {
|
|
@@ -9187,13 +10358,13 @@ async function buildCodexAppProviderCatalogRoutes(provider, apiKey, selectedMode
|
|
|
9187
10358
|
|
|
9188
10359
|
// src/codex/app-config.ts
|
|
9189
10360
|
import { existsSync as existsSync8, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
9190
|
-
import { dirname as dirname2, join as
|
|
10361
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
9191
10362
|
import { parse, stringify } from "smol-toml";
|
|
9192
10363
|
function getCodexConfigPath() {
|
|
9193
|
-
return
|
|
10364
|
+
return join10(getCodexHome(), "config.toml");
|
|
9194
10365
|
}
|
|
9195
10366
|
function getCodexAppSidecarProfilePath() {
|
|
9196
|
-
return
|
|
10367
|
+
return join10(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
|
|
9197
10368
|
}
|
|
9198
10369
|
function asRecord(value) {
|
|
9199
10370
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -9215,16 +10386,20 @@ function applyRestoreNumber(config, key, had, value) {
|
|
|
9215
10386
|
delete config[key];
|
|
9216
10387
|
}
|
|
9217
10388
|
}
|
|
10389
|
+
function isMultiAgentV2Enabled(value) {
|
|
10390
|
+
if (value === true) return true;
|
|
10391
|
+
return asRecord(value).enabled === true;
|
|
10392
|
+
}
|
|
9218
10393
|
function readCodexConfigText(path3 = getCodexConfigPath()) {
|
|
9219
10394
|
if (!existsSync8(path3)) return "";
|
|
9220
10395
|
return readFileSync3(path3, "utf8");
|
|
9221
10396
|
}
|
|
9222
|
-
function parseCodexConfig(
|
|
9223
|
-
if (!
|
|
9224
|
-
return asRecord(parse(
|
|
10397
|
+
function parseCodexConfig(text5) {
|
|
10398
|
+
if (!text5.trim()) return {};
|
|
10399
|
+
return asRecord(parse(text5));
|
|
9225
10400
|
}
|
|
9226
|
-
function captureRestoreState(
|
|
9227
|
-
const config = parseCodexConfig(
|
|
10401
|
+
function captureRestoreState(text5) {
|
|
10402
|
+
const config = parseCodexConfig(text5);
|
|
9228
10403
|
const profile = rootString(config, "profile");
|
|
9229
10404
|
const model = rootString(config, "model");
|
|
9230
10405
|
const modelProvider = rootString(config, "model_provider");
|
|
@@ -9233,6 +10408,8 @@ function captureRestoreState(text4) {
|
|
|
9233
10408
|
const reasoning = rootString(config, "model_reasoning_effort");
|
|
9234
10409
|
const contextWindow = rootNumber(config, "model_context_window");
|
|
9235
10410
|
const autoCompact = rootNumber(config, "model_auto_compact_token_limit");
|
|
10411
|
+
const features = asRecord(config.features);
|
|
10412
|
+
const multiAgentV2 = "multi_agent_v2" in features;
|
|
9236
10413
|
return {
|
|
9237
10414
|
hadProfile: profile.had,
|
|
9238
10415
|
profile: profile.value,
|
|
@@ -9249,16 +10426,18 @@ function captureRestoreState(text4) {
|
|
|
9249
10426
|
hadModelContextWindow: contextWindow.had,
|
|
9250
10427
|
modelContextWindow: contextWindow.value,
|
|
9251
10428
|
hadModelAutoCompactTokenLimit: autoCompact.had,
|
|
9252
|
-
modelAutoCompactTokenLimit: autoCompact.value
|
|
10429
|
+
modelAutoCompactTokenLimit: autoCompact.value,
|
|
10430
|
+
hadMultiAgentV2: multiAgentV2,
|
|
10431
|
+
multiAgentV2: features.multi_agent_v2
|
|
9253
10432
|
};
|
|
9254
10433
|
}
|
|
9255
|
-
function isAppManagedConfig(
|
|
9256
|
-
const config = parseCodexConfig(
|
|
10434
|
+
function isAppManagedConfig(text5) {
|
|
10435
|
+
const config = parseCodexConfig(text5);
|
|
9257
10436
|
const mp = rootString(config, "model_provider");
|
|
9258
10437
|
if (mp.had && mp.value === CODEX_APP_PROVIDER_ID) return true;
|
|
9259
10438
|
const baseUrl = rootString(config, "openai_base_url");
|
|
9260
10439
|
const catalog = rootString(config, "model_catalog_json");
|
|
9261
|
-
return mp.value === "openai" && /^http:\/\/127\.0\.0\.1:\d+\/v1$/.test(baseUrl.value) && /(?:^|[\\/])app-models-[^\\/]+\.json$/.test(catalog.value);
|
|
10440
|
+
return mp.value === "openai" && (/^http:\/\/127\.0\.0\.1:\d+\/v1$/.test(baseUrl.value) || /^http:\/\/127\.0\.0\.1:\d+\/_relay-codex\/[A-Za-z0-9_-]{43}\/v1$/.test(baseUrl.value)) && /(?:^|[\\/])app-models-[^\\/]+\.json$/.test(catalog.value);
|
|
9262
10441
|
}
|
|
9263
10442
|
function mergeAppConfig(existing, spec) {
|
|
9264
10443
|
const patch = buildCodexAppRootConfig(spec);
|
|
@@ -9278,6 +10457,16 @@ function mergeAppConfig(existing, spec) {
|
|
|
9278
10457
|
} else {
|
|
9279
10458
|
delete out.model_auto_compact_token_limit;
|
|
9280
10459
|
}
|
|
10460
|
+
const features = asRecord(out.features);
|
|
10461
|
+
if (spec.multiAgentV2Enabled) {
|
|
10462
|
+
const existingV2 = features.multi_agent_v2;
|
|
10463
|
+
if (existingV2 !== void 0 && !isMultiAgentV2Enabled(existingV2)) {
|
|
10464
|
+
throw new Error("Codex config explicitly disables multi_agent_v2; remove that override before using Codex Sub-agents");
|
|
10465
|
+
}
|
|
10466
|
+
features.multi_agent_v2 = existingV2 ?? patch.features?.["multi_agent_v2"];
|
|
10467
|
+
}
|
|
10468
|
+
if (Object.keys(features).length === 0) delete out.features;
|
|
10469
|
+
else out.features = features;
|
|
9281
10470
|
const providers = asRecord(out.model_providers);
|
|
9282
10471
|
delete providers[CODEX_APP_PROVIDER_ID];
|
|
9283
10472
|
const profiles = asRecord(out.profiles);
|
|
@@ -9311,8 +10500,8 @@ function mergeAppConfig(existing, spec) {
|
|
|
9311
10500
|
}
|
|
9312
10501
|
return out;
|
|
9313
10502
|
}
|
|
9314
|
-
function validateAppConfigText(
|
|
9315
|
-
const config = parseCodexConfig(
|
|
10503
|
+
function validateAppConfigText(text5, spec) {
|
|
10504
|
+
const config = parseCodexConfig(text5);
|
|
9316
10505
|
if ("profile" in config) {
|
|
9317
10506
|
throw new Error("Generated config still contains legacy root profile key");
|
|
9318
10507
|
}
|
|
@@ -9325,13 +10514,17 @@ function validateAppConfigText(text4, spec) {
|
|
|
9325
10514
|
throw new Error("Generated config must keep the built-in OpenAI model_provider");
|
|
9326
10515
|
}
|
|
9327
10516
|
const baseUrl = rootString(config, "openai_base_url");
|
|
9328
|
-
|
|
10517
|
+
const expectedBaseUrl = spec.proxyBaseUrl ?? `http://127.0.0.1:${spec.proxyPort}/v1`;
|
|
10518
|
+
if (baseUrl.value !== expectedBaseUrl) {
|
|
9329
10519
|
throw new Error("Generated config openai_base_url mismatch");
|
|
9330
10520
|
}
|
|
9331
10521
|
const catalog = rootString(config, "model_catalog_json");
|
|
9332
10522
|
if (catalog.value !== spec.catalogPath) {
|
|
9333
10523
|
throw new Error("Generated config model_catalog_json mismatch");
|
|
9334
10524
|
}
|
|
10525
|
+
if (spec.multiAgentV2Enabled && !isMultiAgentV2Enabled(asRecord(config.features).multi_agent_v2)) {
|
|
10526
|
+
throw new Error("Generated config is missing multi_agent_v2 support");
|
|
10527
|
+
}
|
|
9335
10528
|
}
|
|
9336
10529
|
function applyAppConfigPatch(spec, configPath = getCodexConfigPath()) {
|
|
9337
10530
|
const existingText = readCodexConfigText(configPath);
|
|
@@ -9342,12 +10535,12 @@ function applyAppConfigPatch(spec, configPath = getCodexConfigPath()) {
|
|
|
9342
10535
|
throw new Error(`Invalid existing Codex config at ${configPath}: ${err instanceof Error ? err.message : err}`);
|
|
9343
10536
|
}
|
|
9344
10537
|
const merged = mergeAppConfig(existing, spec);
|
|
9345
|
-
const
|
|
10538
|
+
const text5 = `${stringify(merged)}
|
|
9346
10539
|
`;
|
|
9347
|
-
validateAppConfigText(
|
|
10540
|
+
validateAppConfigText(text5, spec);
|
|
9348
10541
|
mkdirSync3(dirname2(configPath), { recursive: true });
|
|
9349
|
-
writeFileSync3(configPath,
|
|
9350
|
-
return
|
|
10542
|
+
writeFileSync3(configPath, text5, "utf8");
|
|
10543
|
+
return text5;
|
|
9351
10544
|
}
|
|
9352
10545
|
function applyRestoreKey(config, key, had, value) {
|
|
9353
10546
|
if (had && value !== void 0) {
|
|
@@ -9366,6 +10559,14 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
|
|
|
9366
10559
|
} else {
|
|
9367
10560
|
config.model_providers = providers;
|
|
9368
10561
|
}
|
|
10562
|
+
const features = asRecord(config.features);
|
|
10563
|
+
if (state.hadMultiAgentV2 && "multiAgentV2" in state) {
|
|
10564
|
+
features.multi_agent_v2 = state.multiAgentV2;
|
|
10565
|
+
} else {
|
|
10566
|
+
delete features.multi_agent_v2;
|
|
10567
|
+
}
|
|
10568
|
+
if (Object.keys(features).length === 0) delete config.features;
|
|
10569
|
+
else config.features = features;
|
|
9369
10570
|
if (state.hadProfile && state.profile) {
|
|
9370
10571
|
config.profile = state.profile;
|
|
9371
10572
|
} else {
|
|
@@ -9399,10 +10600,10 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
|
|
|
9399
10600
|
return true;
|
|
9400
10601
|
}
|
|
9401
10602
|
function previewAppConfigToml(spec) {
|
|
9402
|
-
const
|
|
10603
|
+
const text5 = `${stringify(buildCodexAppRootConfig(spec))}
|
|
9403
10604
|
`;
|
|
9404
|
-
validateAppConfigText(
|
|
9405
|
-
return
|
|
10605
|
+
validateAppConfigText(text5, spec);
|
|
10606
|
+
return text5;
|
|
9406
10607
|
}
|
|
9407
10608
|
|
|
9408
10609
|
// src/codex/app-session.ts
|
|
@@ -9412,17 +10613,18 @@ import {
|
|
|
9412
10613
|
mkdirSync as mkdirSync4,
|
|
9413
10614
|
readdirSync as readdirSync2,
|
|
9414
10615
|
readFileSync as readFileSync4,
|
|
9415
|
-
rmSync as rmSync4
|
|
10616
|
+
rmSync as rmSync4,
|
|
10617
|
+
statSync as statSync2
|
|
9416
10618
|
} from "fs";
|
|
9417
|
-
import { basename as basename2, join as
|
|
10619
|
+
import { basename as basename2, join as join11 } from "path";
|
|
9418
10620
|
function getAppSessionLockPath(env = process.env) {
|
|
9419
|
-
return
|
|
10621
|
+
return join11(getRelayAiCodexDir(env), "session-app.json");
|
|
9420
10622
|
}
|
|
9421
10623
|
function getAppRestoreStatePath(env = process.env) {
|
|
9422
|
-
return
|
|
10624
|
+
return join11(getRelayAiCodexDir(env), "app-restore-state.json");
|
|
9423
10625
|
}
|
|
9424
10626
|
function getAppCatalogPath(providerId, env = process.env) {
|
|
9425
|
-
return
|
|
10627
|
+
return join11(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
|
|
9426
10628
|
}
|
|
9427
10629
|
function readAppSessionLock(env = process.env) {
|
|
9428
10630
|
const path3 = getAppSessionLockPath(env);
|
|
@@ -9467,24 +10669,24 @@ function backupConfigToml(env = process.env) {
|
|
|
9467
10669
|
const backupsDir = getBackupsDir(env);
|
|
9468
10670
|
mkdirSync4(backupsDir, { recursive: true });
|
|
9469
10671
|
const base = basename2(configPath);
|
|
9470
|
-
const backupPath =
|
|
10672
|
+
const backupPath = join11(backupsDir, `${base}.${Date.now()}.bak`);
|
|
9471
10673
|
copyFileSync2(configPath, backupPath);
|
|
9472
10674
|
return backupPath;
|
|
9473
10675
|
}
|
|
9474
10676
|
function saveAppRestoreStateBeforePatch(env = process.env) {
|
|
9475
|
-
const
|
|
10677
|
+
const text5 = readCodexConfigText();
|
|
9476
10678
|
const existing = readAppRestoreState(env);
|
|
9477
|
-
if (existing && isAppManagedConfig(
|
|
10679
|
+
if (existing && isAppManagedConfig(text5)) {
|
|
9478
10680
|
return existing;
|
|
9479
10681
|
}
|
|
9480
|
-
const state = captureRestoreState(
|
|
10682
|
+
const state = captureRestoreState(text5);
|
|
9481
10683
|
writeAppRestoreState(state, env);
|
|
9482
10684
|
return state;
|
|
9483
10685
|
}
|
|
9484
10686
|
function ownedAppCatalogPaths(env = process.env) {
|
|
9485
10687
|
const codexDir = getRelayAiCodexDir(env);
|
|
9486
10688
|
if (!existsSync9(codexDir)) return [];
|
|
9487
|
-
return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) =>
|
|
10689
|
+
return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) => join11(codexDir, n));
|
|
9488
10690
|
}
|
|
9489
10691
|
function removeAppCatalogs(env = process.env) {
|
|
9490
10692
|
const removed = [];
|
|
@@ -9497,6 +10699,20 @@ function removeAppCatalogs(env = process.env) {
|
|
|
9497
10699
|
}
|
|
9498
10700
|
return removed;
|
|
9499
10701
|
}
|
|
10702
|
+
function newestConfigBackup(env = process.env) {
|
|
10703
|
+
const backupDir = getBackupsDir(env);
|
|
10704
|
+
if (!existsSync9(backupDir)) return null;
|
|
10705
|
+
const configBase = basename2(getCodexConfigPath());
|
|
10706
|
+
const candidates = readdirSync2(backupDir).filter((name) => name.startsWith(`${configBase}.`) && name.endsWith(".bak")).map((name) => {
|
|
10707
|
+
const path3 = join11(backupDir, name);
|
|
10708
|
+
try {
|
|
10709
|
+
return { path: path3, mtimeMs: statSync2(path3).mtimeMs };
|
|
10710
|
+
} catch {
|
|
10711
|
+
return null;
|
|
10712
|
+
}
|
|
10713
|
+
}).filter((entry) => entry !== null).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
10714
|
+
return candidates[0]?.path ?? null;
|
|
10715
|
+
}
|
|
9500
10716
|
function restoreCodexAppOverlay(env = process.env) {
|
|
9501
10717
|
const lock = readAppSessionLock(env);
|
|
9502
10718
|
if (lock && lock.pid !== process.pid && isConcurrentSession(lock)) {
|
|
@@ -9506,8 +10722,8 @@ function restoreCodexAppOverlay(env = process.env) {
|
|
|
9506
10722
|
message: `Another relay-ai codex-app session is running (pid ${lock.pid}). Ctrl+C it first, then run --restore.`
|
|
9507
10723
|
};
|
|
9508
10724
|
}
|
|
9509
|
-
const
|
|
9510
|
-
const managed = isAppManagedConfig(
|
|
10725
|
+
const text5 = readCodexConfigText();
|
|
10726
|
+
const managed = isAppManagedConfig(text5);
|
|
9511
10727
|
const restoreState = readAppRestoreState(env);
|
|
9512
10728
|
if (!managed && !restoreState && !lock) {
|
|
9513
10729
|
removeAppCatalogs(env);
|
|
@@ -9518,6 +10734,9 @@ function restoreCodexAppOverlay(env = process.env) {
|
|
|
9518
10734
|
restoreConfigFromState(restoreState);
|
|
9519
10735
|
} else if (lock?.backupPath && existsSync9(lock.backupPath)) {
|
|
9520
10736
|
copyFileSync2(lock.backupPath, getCodexConfigPath());
|
|
10737
|
+
} else if (managed) {
|
|
10738
|
+
const backupPath = newestConfigBackup(env);
|
|
10739
|
+
if (backupPath) copyFileSync2(backupPath, getCodexConfigPath());
|
|
9521
10740
|
}
|
|
9522
10741
|
removeAppCatalogs(env);
|
|
9523
10742
|
clearAppRestoreState(env);
|
|
@@ -9583,7 +10802,8 @@ function codexProxyRouteToCodexRoute(route, fallbackProviderId) {
|
|
|
9583
10802
|
supportedParameters: route.supportedParameters,
|
|
9584
10803
|
reasoning: route.reasoning,
|
|
9585
10804
|
interleavedReasoningField: route.interleavedReasoningField,
|
|
9586
|
-
headers: route.headers
|
|
10805
|
+
headers: route.headers,
|
|
10806
|
+
refreshToken: route.refreshToken
|
|
9587
10807
|
};
|
|
9588
10808
|
}
|
|
9589
10809
|
async function waitForShutdownWithConfirm() {
|
|
@@ -9624,6 +10844,8 @@ ${pc10.bold("Usage:")}
|
|
|
9624
10844
|
|
|
9625
10845
|
${pc10.bold("Options:")}
|
|
9626
10846
|
--vertex Use Claude models through Google Vertex AI
|
|
10847
|
+
--with-native Load native Codex models beside Relay models for this launch
|
|
10848
|
+
--relay-only Keep the current Relay-only launch behavior
|
|
9627
10849
|
--restore Restore Codex config after an interrupted app session
|
|
9628
10850
|
--config Preview the generated Codex app configuration without launching
|
|
9629
10851
|
--trace Write proxy debug logs to ~/.relay-ai/logs/ and show errors on exit
|
|
@@ -9636,7 +10858,7 @@ ${pc10.bold("Description:")}
|
|
|
9636
10858
|
ChatGPT desktop app in Codex mode. Keep this terminal open while using Codex.
|
|
9637
10859
|
|
|
9638
10860
|
${pc10.bold("Platforms:")}
|
|
9639
|
-
macOS and
|
|
10861
|
+
macOS, Windows, and Linux (ChatGPT desktop app preview).
|
|
9640
10862
|
|
|
9641
10863
|
${pc10.bold("Cleanup:")}
|
|
9642
10864
|
Ctrl+C stops the proxy and restores your previous Codex config.
|
|
@@ -9865,7 +11087,13 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
9865
11087
|
}
|
|
9866
11088
|
const prefs = loadPreferences();
|
|
9867
11089
|
const favorites = prefs.favoriteModels ?? [];
|
|
9868
|
-
|
|
11090
|
+
let mixedMode = opts.codexLaunchMode === "mixed";
|
|
11091
|
+
if (!configOnly && isTty && !(opts.launchProvider && opts.launchModel) && opts.codexLaunchMode === void 0) {
|
|
11092
|
+
const selectedLaunchMode = await pickCodexLaunchMode();
|
|
11093
|
+
if (!selectedLaunchMode) return 0;
|
|
11094
|
+
mixedMode = selectedLaunchMode === "mixed";
|
|
11095
|
+
}
|
|
11096
|
+
const favoritesActive = favorites.length > 0 && !mixedMode;
|
|
9869
11097
|
if (favoritesActive && !configOnly) {
|
|
9870
11098
|
p12.log.info(
|
|
9871
11099
|
`Favorites mode active \u2014 Codex App picker will show ${favorites.length + 1} models (1 starting + ${favorites.length} favorites).`
|
|
@@ -9929,7 +11157,7 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
9929
11157
|
activeProvider.apiKey = apiKey;
|
|
9930
11158
|
let cloudCodeBackend = null;
|
|
9931
11159
|
let cloudCodeBackendFav = null;
|
|
9932
|
-
const appProviderRoutes = favoritesActive ? null : await buildCodexAppProviderCatalogRoutes(activeProvider, apiKey, selectedModel.id, trace);
|
|
11160
|
+
const appProviderRoutes = mixedMode || favoritesActive ? null : await buildCodexAppProviderCatalogRoutes(activeProvider, apiKey, selectedModel.id, trace);
|
|
9933
11161
|
cloudCodeBackend = appProviderRoutes?.backend ?? null;
|
|
9934
11162
|
const route = appProviderRoutes ? codexProxyRouteToCodexRoute(appProviderRoutes.selectedRoute, activeProvider.id) : resolveCodexRoute(activeProvider, selectedModel, apiKey);
|
|
9935
11163
|
const appRoute = { ...route, tier: "proxy" };
|
|
@@ -9942,6 +11170,42 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
9942
11170
|
resolvedFavorites = res.resolvedFavorites;
|
|
9943
11171
|
providersById = res.providersById;
|
|
9944
11172
|
}
|
|
11173
|
+
let mixedPlan = null;
|
|
11174
|
+
if (mixedMode) {
|
|
11175
|
+
try {
|
|
11176
|
+
const embeddedBinary = findEmbeddedCodexBinary();
|
|
11177
|
+
if (!embeddedBinary) throw new Error("Embedded ChatGPT/Codex runtime was not found; mixed Desktop mode is unavailable on this installation");
|
|
11178
|
+
const version = execFileSync5(embeddedBinary, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
11179
|
+
const mixedModels = await resolveCodexMixedModels({
|
|
11180
|
+
activeProvider,
|
|
11181
|
+
selectedModel,
|
|
11182
|
+
compatible,
|
|
11183
|
+
generalFavorites: favorites,
|
|
11184
|
+
subagentFavorites: prefs.codexSubagentModels ?? []
|
|
11185
|
+
});
|
|
11186
|
+
assertConfiguredCodexSubagentsResolved(prefs.codexSubagentModels ?? [], mixedModels);
|
|
11187
|
+
const multiAgentV2Supported = mixedModels.subagents.length === 0 || supportsMultiAgentV2(embeddedBinary);
|
|
11188
|
+
if (!multiAgentV2Supported) {
|
|
11189
|
+
throw new Error("This ChatGPT/Codex runtime does not support multi_agent_v2, which is required for the configured Codex SubAgent");
|
|
11190
|
+
}
|
|
11191
|
+
const nativeCatalog = await captureNativeCodexCatalog({ target: "app", binaryPath: embeddedBinary, codexVersion: version });
|
|
11192
|
+
const preparedRoutes = await prepareCodexMixedRelayRoutes(mixedModels, trace);
|
|
11193
|
+
cloudCodeBackendFav = preparedRoutes.cloudCodeBackend;
|
|
11194
|
+
mixedPlan = buildCodexMixedLaunchPlan({
|
|
11195
|
+
nativeCatalog,
|
|
11196
|
+
models: mixedModels,
|
|
11197
|
+
relayRoutes: preparedRoutes.routes,
|
|
11198
|
+
multiAgentV2Supported
|
|
11199
|
+
});
|
|
11200
|
+
} catch (err) {
|
|
11201
|
+
cloudCodeBackendFav?.handle.close();
|
|
11202
|
+
cloudCodeBackendFav = null;
|
|
11203
|
+
console.error(pc10.red(`
|
|
11204
|
+
Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}`));
|
|
11205
|
+
console.error("Use relay-ai codex-app --relay-only to continue with Relay models.");
|
|
11206
|
+
return 1;
|
|
11207
|
+
}
|
|
11208
|
+
}
|
|
9945
11209
|
if (!configOnly) {
|
|
9946
11210
|
const modelLabel = formatCodexModelLabel(selectedModel);
|
|
9947
11211
|
const confirmed = await confirmCodexLaunch(
|
|
@@ -9958,8 +11222,16 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
9958
11222
|
let proxyHandle = null;
|
|
9959
11223
|
let sessionActive = false;
|
|
9960
11224
|
try {
|
|
9961
|
-
const catalogPath = favoritesActive && resolvedFavorites.length > 0 ? getFavoritesAppCatalogPath() : getAppCatalogPath(route.providerId);
|
|
9962
|
-
const activeRoute =
|
|
11225
|
+
const catalogPath = mixedPlan ? join12(getRelayAiCodexDir(), "app-models-mixed.json") : favoritesActive && resolvedFavorites.length > 0 ? getFavoritesAppCatalogPath() : getAppCatalogPath(route.providerId);
|
|
11226
|
+
const activeRoute = mixedPlan ? {
|
|
11227
|
+
tier: "proxy",
|
|
11228
|
+
modelId: mixedPlan.selectedSlug,
|
|
11229
|
+
providerId: activeProvider.id,
|
|
11230
|
+
npm: "",
|
|
11231
|
+
upstreamModelId: "",
|
|
11232
|
+
apiKey: "",
|
|
11233
|
+
contextWindow: selectedModel.contextWindow
|
|
11234
|
+
} : favoritesActive && resolvedFavorites.length > 0 ? {
|
|
9963
11235
|
tier: "proxy",
|
|
9964
11236
|
modelId: codexCliFavoritesSlug(activeProvider.id, selectedModel.id),
|
|
9965
11237
|
providerId: activeProvider.id,
|
|
@@ -9975,7 +11247,11 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
9975
11247
|
console.log("");
|
|
9976
11248
|
console.log(pc10.bold(pc10.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app")));
|
|
9977
11249
|
console.log("");
|
|
9978
|
-
if (
|
|
11250
|
+
if (mixedPlan) {
|
|
11251
|
+
console.log(` ${pc10.bold("Mode:")} Native + Relay mixed catalog`);
|
|
11252
|
+
console.log(` ${pc10.bold("Native:")} ${mixedPlan.nativeModelIds.size} native Codex models`);
|
|
11253
|
+
console.log(` ${pc10.bold("Relay:")} ${mixedPlan.relayRoutes.length} Relay routes (${mixedPlan.subagentModelCount} Codex SubAgent model)`);
|
|
11254
|
+
} else if (favoritesActive) {
|
|
9979
11255
|
console.log(` ${pc10.bold("Mode:")} Favorites Catalog (${resolvedFavorites.length} model${resolvedFavorites.length !== 1 ? "s" : ""})`);
|
|
9980
11256
|
console.log("");
|
|
9981
11257
|
console.log(` ${pc10.bold("Models:")}`);
|
|
@@ -9992,7 +11268,9 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
9992
11268
|
console.log(` ${pc10.bold("config.toml patch preview:")}`);
|
|
9993
11269
|
const tomlPreview = previewAppConfigToml({
|
|
9994
11270
|
...specBase,
|
|
9995
|
-
proxyPort: PREVIEW_PROXY_PORT
|
|
11271
|
+
proxyPort: PREVIEW_PROXY_PORT,
|
|
11272
|
+
...mixedPlan?.multiAgentV2Enabled ? { multiAgentV2Enabled: true } : {},
|
|
11273
|
+
...mixedPlan ? { proxyBaseUrl: `${mixedProxyBaseUrl(PREVIEW_PROXY_PORT, mixedPlan.capability)}/v1` } : {}
|
|
9996
11274
|
});
|
|
9997
11275
|
for (const line of tomlPreview.split("\n")) {
|
|
9998
11276
|
console.log(` ${pc10.dim(line)}`);
|
|
@@ -10007,7 +11285,19 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
10007
11285
|
return 0;
|
|
10008
11286
|
}
|
|
10009
11287
|
let proxyPort;
|
|
10010
|
-
if (
|
|
11288
|
+
if (mixedPlan) {
|
|
11289
|
+
proxyHandle = await startCodexProxy(mixedPlan.relayRoutes, {
|
|
11290
|
+
requireAuth: false,
|
|
11291
|
+
debug: trace,
|
|
11292
|
+
mixedNative: {
|
|
11293
|
+
nativeModelIds: mixedPlan.nativeModelIds,
|
|
11294
|
+
subagentRouteModelId: mixedPlan.subagentRouteModelId,
|
|
11295
|
+
capability: mixedPlan.capability,
|
|
11296
|
+
nativePayloadRelayModel: mixedPlan.nativePayloadRelayModel
|
|
11297
|
+
}
|
|
11298
|
+
});
|
|
11299
|
+
proxyPort = proxyHandle.port;
|
|
11300
|
+
} else if (favoritesActive && resolvedFavorites.length > 0) {
|
|
10011
11301
|
const needsBackend = (r) => {
|
|
10012
11302
|
const m = r.model;
|
|
10013
11303
|
const prov = providersById.get(r.providerId);
|
|
@@ -10056,12 +11346,14 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
10056
11346
|
proxyPort = proxyHandle.port;
|
|
10057
11347
|
}
|
|
10058
11348
|
const modelLabel = formatCodexModelLabel(selectedModel);
|
|
10059
|
-
const catalogFile = favoritesActive && resolvedFavorites.length > 0 ? buildFavoritesAppCatalog(resolvedFavorites) : buildAppCatalogFile(catalogModels, activeProvider.name, appRoute.modelId);
|
|
11349
|
+
const catalogFile = mixedPlan ? mixedPlan.catalog : favoritesActive && resolvedFavorites.length > 0 ? buildFavoritesAppCatalog(resolvedFavorites) : buildAppCatalogFile(catalogModels, activeProvider.name, appRoute.modelId);
|
|
10060
11350
|
writeOverlayFile(catalogPath, serializeCatalog(catalogFile));
|
|
10061
11351
|
const spec = {
|
|
10062
11352
|
route: activeRoute,
|
|
10063
11353
|
proxyPort,
|
|
10064
|
-
catalogPath
|
|
11354
|
+
catalogPath,
|
|
11355
|
+
...mixedPlan?.multiAgentV2Enabled ? { multiAgentV2Enabled: true } : {},
|
|
11356
|
+
...mixedPlan ? { proxyBaseUrl: `${mixedProxyBaseUrl(proxyPort, mixedPlan.capability)}/v1` } : {}
|
|
10065
11357
|
};
|
|
10066
11358
|
saveAppRestoreStateBeforePatch();
|
|
10067
11359
|
const backupPath = backupConfigToml();
|
|
@@ -10126,22 +11418,22 @@ import * as p13 from "@clack/prompts";
|
|
|
10126
11418
|
// src/claude-desktop/app-config.ts
|
|
10127
11419
|
import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
|
|
10128
11420
|
import { homedir as homedir9 } from "os";
|
|
10129
|
-
import { join as
|
|
11421
|
+
import { join as join13, dirname as dirname3 } from "path";
|
|
10130
11422
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
10131
11423
|
function getClaudeDesktopHome() {
|
|
10132
11424
|
if (process.platform === "win32") {
|
|
10133
|
-
return
|
|
11425
|
+
return join13(process.env.LOCALAPPDATA || join13(homedir9(), "AppData", "Local"), "Claude-3p");
|
|
10134
11426
|
}
|
|
10135
11427
|
if (process.platform === "linux") {
|
|
10136
|
-
return
|
|
11428
|
+
return join13(process.env.XDG_CONFIG_HOME || join13(homedir9(), ".config"), "Claude-3p");
|
|
10137
11429
|
}
|
|
10138
|
-
return
|
|
11430
|
+
return join13(homedir9(), "Library", "Application Support", "Claude-3p");
|
|
10139
11431
|
}
|
|
10140
11432
|
function getConfigLibraryPath() {
|
|
10141
|
-
return
|
|
11433
|
+
return join13(getClaudeDesktopHome(), "configLibrary");
|
|
10142
11434
|
}
|
|
10143
11435
|
function getMetaJsonPath() {
|
|
10144
|
-
return
|
|
11436
|
+
return join13(getConfigLibraryPath(), "_meta.json");
|
|
10145
11437
|
}
|
|
10146
11438
|
function readMetaJson() {
|
|
10147
11439
|
const metaPath = getMetaJsonPath();
|
|
@@ -10169,7 +11461,7 @@ function buildRelayAiConfig(proxyPort) {
|
|
|
10169
11461
|
}
|
|
10170
11462
|
function writeRelayAiConfig(proxyPort) {
|
|
10171
11463
|
const uuid = randomUUID2();
|
|
10172
|
-
const configPath =
|
|
11464
|
+
const configPath = join13(getConfigLibraryPath(), `${uuid}.json`);
|
|
10173
11465
|
const config = buildRelayAiConfig(proxyPort);
|
|
10174
11466
|
mkdirSync5(dirname3(configPath), { recursive: true });
|
|
10175
11467
|
writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
|
|
@@ -10334,9 +11626,9 @@ import {
|
|
|
10334
11626
|
unlinkSync as unlinkSync2,
|
|
10335
11627
|
writeFileSync as writeFileSync5
|
|
10336
11628
|
} from "fs";
|
|
10337
|
-
import { dirname as dirname4, join as
|
|
11629
|
+
import { dirname as dirname4, join as join14 } from "path";
|
|
10338
11630
|
function getSessionLockPath2() {
|
|
10339
|
-
return
|
|
11631
|
+
return join14(getClaudeDesktopHome(), ".relay-ai.lock");
|
|
10340
11632
|
}
|
|
10341
11633
|
function inspectSessionLock() {
|
|
10342
11634
|
const path3 = getSessionLockPath2();
|
|
@@ -10390,7 +11682,7 @@ function restoreMetaJson() {
|
|
|
10390
11682
|
}
|
|
10391
11683
|
}
|
|
10392
11684
|
function removeRelayAiConfig(uuid) {
|
|
10393
|
-
const configPath =
|
|
11685
|
+
const configPath = join14(getConfigLibraryPath(), `${uuid}.json`);
|
|
10394
11686
|
if (existsSync11(configPath)) {
|
|
10395
11687
|
try {
|
|
10396
11688
|
rmSync5(configPath, { force: true });
|
|
@@ -10710,15 +12002,15 @@ ${pc11.bold("Claude Desktop 3P Mode Active")}`);
|
|
|
10710
12002
|
// src/ai-doc.ts
|
|
10711
12003
|
import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
10712
12004
|
import { homedir as homedir10 } from "os";
|
|
10713
|
-
import { join as
|
|
12005
|
+
import { join as join15 } from "path";
|
|
10714
12006
|
var SKILL_DIR_NAME = "relay-ai-cli";
|
|
10715
12007
|
var SKILL_INSTALL_DIRS = [
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
12008
|
+
join15(getAppHome(), "skills"),
|
|
12009
|
+
join15(homedir10(), ".claude", "skills"),
|
|
12010
|
+
join15(homedir10(), ".agents", "skills"),
|
|
12011
|
+
join15(homedir10(), ".codex", "skills"),
|
|
12012
|
+
join15(homedir10(), ".cursor", "skills"),
|
|
12013
|
+
join15(homedir10(), ".cursor", "skills-cursor")
|
|
10722
12014
|
];
|
|
10723
12015
|
function parseSkillVersion(content) {
|
|
10724
12016
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -10732,7 +12024,7 @@ function parseSkillVersion(content) {
|
|
|
10732
12024
|
return null;
|
|
10733
12025
|
}
|
|
10734
12026
|
function readInstalledSkillVersion(skillDir) {
|
|
10735
|
-
const skillPath =
|
|
12027
|
+
const skillPath = join15(skillDir, "SKILL.md");
|
|
10736
12028
|
if (!existsSync12(skillPath)) return null;
|
|
10737
12029
|
try {
|
|
10738
12030
|
const head = readFileSync7(skillPath, "utf-8").slice(0, 1024);
|
|
@@ -10745,8 +12037,8 @@ function readInstalledSkillVersion(skillDir) {
|
|
|
10745
12037
|
}
|
|
10746
12038
|
function skillInstallTargets() {
|
|
10747
12039
|
return SKILL_INSTALL_DIRS.map((dir) => {
|
|
10748
|
-
const skillDir =
|
|
10749
|
-
return { skillDir, skillPath:
|
|
12040
|
+
const skillDir = join15(dir, SKILL_DIR_NAME);
|
|
12041
|
+
return { skillDir, skillPath: join15(skillDir, "SKILL.md") };
|
|
10750
12042
|
});
|
|
10751
12043
|
}
|
|
10752
12044
|
function formatProviderModels(provider) {
|
|
@@ -10961,6 +12253,14 @@ FAVORITES / MID-SESSION SWITCHING:
|
|
|
10961
12253
|
Exception: Claude --http-proxy combines the selected compatible model with
|
|
10962
12254
|
compatible saved favorites while keeping native Anthropic models available.
|
|
10963
12255
|
|
|
12256
|
+
CODEX SUBAGENT / MIXED NATIVE MODE:
|
|
12257
|
+
relay-ai subagents manage the separate one-model Codex SubAgent catalog
|
|
12258
|
+
The catalog starts empty and never imports or synchronizes with General Favorites.
|
|
12259
|
+
In mixed mode, Codex decides when to launch a sub-agent and Relay routes every
|
|
12260
|
+
Codex-marked child to the configured Codex SubAgent model.
|
|
12261
|
+
Enable native models alongside Relay with --with-native on codex/codex-app, or
|
|
12262
|
+
use the same option in the Relay UI launch card. Use --relay-only to opt out.
|
|
12263
|
+
|
|
10964
12264
|
================================================================================
|
|
10965
12265
|
COMMANDS
|
|
10966
12266
|
================================================================================
|
|
@@ -11041,14 +12341,16 @@ PROVIDERS REGISTRY
|
|
|
11041
12341
|
MODELS / FAVORITES
|
|
11042
12342
|
relay-ai models manage favoriteModels in config (alias: favorites)
|
|
11043
12343
|
Used for mid-session /model switching in interactive Claude/Codex/Gemini sessions.
|
|
12344
|
+
relay-ai subagents manage the separate one-model Codex SubAgent catalog.
|
|
11044
12345
|
|
|
11045
12346
|
API GATEWAY (for tools that speak Anthropic/OpenAI HTTP)
|
|
11046
12347
|
relay-ai server foreground gateway on port 17645
|
|
11047
12348
|
relay-ai server --vertex Vertex AI gateway (gcloud ADC)
|
|
11048
12349
|
|
|
11049
12350
|
DESKTOP APPS
|
|
11050
|
-
relay-ai codex-app ChatGPT desktop, Codex mode (macOS/Windows); alias: chatgpt
|
|
11051
|
-
relay-ai claude-app Claude desktop (macOS/Windows)
|
|
12351
|
+
relay-ai codex-app ChatGPT desktop, Codex mode (macOS/Windows/Linux); alias: chatgpt
|
|
12352
|
+
relay-ai claude-app Claude desktop (macOS/Windows/Linux)
|
|
12353
|
+
Linux desktop launches resolve the X11/RDP display from the terminal WINDOWID.
|
|
11052
12354
|
|
|
11053
12355
|
================================================================================
|
|
11054
12356
|
CONFIGURATION PATHS
|
|
@@ -11399,24 +12701,24 @@ function buildHttpProxyChildEnv(baseEnv, proxyUrl, caCertPath) {
|
|
|
11399
12701
|
}
|
|
11400
12702
|
|
|
11401
12703
|
// src/http-proxy/ca.ts
|
|
11402
|
-
import { randomBytes, randomUUID as randomUUID3 } from "crypto";
|
|
12704
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "crypto";
|
|
11403
12705
|
import {
|
|
11404
|
-
chmodSync,
|
|
12706
|
+
chmodSync as chmodSync2,
|
|
11405
12707
|
existsSync as existsSync13,
|
|
11406
12708
|
mkdirSync as mkdirSync9,
|
|
11407
12709
|
readFileSync as readFileSync8,
|
|
11408
12710
|
readdirSync as readdirSync3,
|
|
11409
12711
|
rmSync as rmSync6,
|
|
11410
|
-
statSync as
|
|
12712
|
+
statSync as statSync3,
|
|
11411
12713
|
writeFileSync as writeFileSync8
|
|
11412
12714
|
} from "fs";
|
|
11413
|
-
import { dirname as dirname6, join as
|
|
12715
|
+
import { dirname as dirname6, join as join17, resolve } from "path";
|
|
11414
12716
|
import forge from "node-forge";
|
|
11415
12717
|
var SESSION_ROOT = "http-proxy-sessions";
|
|
11416
12718
|
var OWNER_FILE = "owner.pid";
|
|
11417
12719
|
var MID_CREATION_GRACE_MS = 3e4;
|
|
11418
12720
|
function serialNumber() {
|
|
11419
|
-
const bytes =
|
|
12721
|
+
const bytes = randomBytes2(16);
|
|
11420
12722
|
bytes[0] &= 127;
|
|
11421
12723
|
return bytes.toString("hex");
|
|
11422
12724
|
}
|
|
@@ -11430,15 +12732,15 @@ function processIsRunning(pid) {
|
|
|
11430
12732
|
}
|
|
11431
12733
|
}
|
|
11432
12734
|
function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
|
|
11433
|
-
const root =
|
|
12735
|
+
const root = join17(appHome, SESSION_ROOT);
|
|
11434
12736
|
if (!existsSync13(root)) return;
|
|
11435
12737
|
const now = Date.now();
|
|
11436
12738
|
for (const name of readdirSync3(root)) {
|
|
11437
|
-
const sessionDir =
|
|
12739
|
+
const sessionDir = join17(root, name);
|
|
11438
12740
|
try {
|
|
11439
|
-
const stat =
|
|
12741
|
+
const stat = statSync3(sessionDir);
|
|
11440
12742
|
if (!stat.isDirectory()) continue;
|
|
11441
|
-
const ownerPath =
|
|
12743
|
+
const ownerPath = join17(sessionDir, OWNER_FILE);
|
|
11442
12744
|
if (!existsSync13(ownerPath)) {
|
|
11443
12745
|
if (now - stat.mtimeMs > MID_CREATION_GRACE_MS) {
|
|
11444
12746
|
rmSync6(sessionDir, { recursive: true, force: true });
|
|
@@ -11447,7 +12749,7 @@ function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
|
|
|
11447
12749
|
}
|
|
11448
12750
|
const pid = Number(readFileSync8(ownerPath, "utf8").trim());
|
|
11449
12751
|
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
11450
|
-
const ownerStat =
|
|
12752
|
+
const ownerStat = statSync3(ownerPath);
|
|
11451
12753
|
const newestMtimeMs = Math.max(stat.mtimeMs, ownerStat.mtimeMs);
|
|
11452
12754
|
if (now - newestMtimeMs > MID_CREATION_GRACE_MS) {
|
|
11453
12755
|
rmSync6(sessionDir, { recursive: true, force: true });
|
|
@@ -11461,13 +12763,13 @@ function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
|
|
|
11461
12763
|
}
|
|
11462
12764
|
function createHttpProxyCertificates(appHome = getAppHome()) {
|
|
11463
12765
|
cleanupStaleHttpProxySessions(appHome);
|
|
11464
|
-
const root =
|
|
12766
|
+
const root = join17(appHome, SESSION_ROOT);
|
|
11465
12767
|
mkdirSync9(root, { recursive: true, mode: 448 });
|
|
11466
|
-
|
|
11467
|
-
const sessionDir =
|
|
12768
|
+
chmodSync2(root, 448);
|
|
12769
|
+
const sessionDir = join17(root, randomUUID3());
|
|
11468
12770
|
mkdirSync9(sessionDir, { mode: 448 });
|
|
11469
|
-
|
|
11470
|
-
writeFileSync8(
|
|
12771
|
+
chmodSync2(sessionDir, 448);
|
|
12772
|
+
writeFileSync8(join17(sessionDir, OWNER_FILE), `${process.pid}
|
|
11471
12773
|
`, { mode: 384 });
|
|
11472
12774
|
try {
|
|
11473
12775
|
const caKeys = forge.pki.rsa.generateKeyPair(2048);
|
|
@@ -11502,9 +12804,9 @@ function createHttpProxyCertificates(appHome = getAppHome()) {
|
|
|
11502
12804
|
]);
|
|
11503
12805
|
server.sign(caKeys.privateKey, forge.md.sha256.create());
|
|
11504
12806
|
const caCert = forge.pki.certificateToPem(ca);
|
|
11505
|
-
const caCertPath =
|
|
12807
|
+
const caCertPath = join17(sessionDir, "relay-ai-ca.pem");
|
|
11506
12808
|
writeFileSync8(caCertPath, caCert, { encoding: "utf8", mode: 384 });
|
|
11507
|
-
|
|
12809
|
+
chmodSync2(caCertPath, 384);
|
|
11508
12810
|
let cleaned = false;
|
|
11509
12811
|
const cleanupOnExit = () => {
|
|
11510
12812
|
if (cleaned) return;
|
|
@@ -11548,7 +12850,7 @@ function createHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
|
|
|
11548
12850
|
const relayCa = readFileSync8(relayCaCertPath, "utf8").trimEnd();
|
|
11549
12851
|
const additionalCa = readFileSync8(additionalCaCertPath, "utf8").trim();
|
|
11550
12852
|
if (!additionalCa) return relayCaCertPath;
|
|
11551
|
-
const combinedPath =
|
|
12853
|
+
const combinedPath = join17(dirname6(relayCaCertPath), "combined-ca.pem");
|
|
11552
12854
|
writeFileSync8(
|
|
11553
12855
|
combinedPath,
|
|
11554
12856
|
`${relayCa}
|
|
@@ -11556,7 +12858,7 @@ ${additionalCa}
|
|
|
11556
12858
|
`,
|
|
11557
12859
|
{ encoding: "utf8", mode: 384 }
|
|
11558
12860
|
);
|
|
11559
|
-
|
|
12861
|
+
chmodSync2(combinedPath, 384);
|
|
11560
12862
|
return combinedPath;
|
|
11561
12863
|
}
|
|
11562
12864
|
|
|
@@ -11564,7 +12866,7 @@ ${additionalCa}
|
|
|
11564
12866
|
import * as http2 from "http";
|
|
11565
12867
|
import * as https from "https";
|
|
11566
12868
|
import * as net from "net";
|
|
11567
|
-
import { randomBytes as
|
|
12869
|
+
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
11568
12870
|
import { URL as URL2 } from "url";
|
|
11569
12871
|
var ANTHROPIC_HOST = RELAY_SENTINEL_HOST;
|
|
11570
12872
|
var MAX_BODY_BYTES = 50 * 1024 * 1024;
|
|
@@ -11950,7 +13252,7 @@ async function startHttpProxy(options) {
|
|
|
11950
13252
|
});
|
|
11951
13253
|
mitmServer.on("tlsClientError", () => {
|
|
11952
13254
|
});
|
|
11953
|
-
const password3 =
|
|
13255
|
+
const password3 = randomBytes3(32).toString("base64url");
|
|
11954
13256
|
const expectedAuthorization = `Basic ${Buffer.from(`${PROXY_USERNAME}:${password3}`).toString("base64")}`;
|
|
11955
13257
|
const sockets = /* @__PURE__ */ new Set();
|
|
11956
13258
|
const proxyServer = http2.createServer((req, res) => {
|
|
@@ -12260,14 +13562,26 @@ function parseArgs(args) {
|
|
|
12260
13562
|
}
|
|
12261
13563
|
return parsed2;
|
|
12262
13564
|
}
|
|
13565
|
+
if (first === "subagents") {
|
|
13566
|
+
const parsed2 = emptyParsed("models");
|
|
13567
|
+
parsed2.modelCatalogScope = "codex-subagents";
|
|
13568
|
+
for (const arg of rest) {
|
|
13569
|
+
if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
|
|
13570
|
+
else if (arg === "--version" || arg === "-v") parsed2.showVersion = true;
|
|
13571
|
+
else if (!parsed2.error) parsed2.error = "subagents does not accept model catalog flags";
|
|
13572
|
+
}
|
|
13573
|
+
return parsed2;
|
|
13574
|
+
}
|
|
12263
13575
|
if (first === "models" || first === "favorites") {
|
|
12264
13576
|
const parsed2 = emptyParsed("models");
|
|
13577
|
+
parsed2.modelCatalogScope = "global";
|
|
12265
13578
|
for (const arg of rest) {
|
|
12266
13579
|
if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
|
|
12267
13580
|
else if (arg === "--version" || arg === "-v") parsed2.showVersion = true;
|
|
12268
13581
|
else if (arg === "--agy") parsed2.favoritesAgy = true;
|
|
12269
13582
|
else if (!parsed2.error) parsed2.error = `Unknown models option: ${arg}`;
|
|
12270
13583
|
}
|
|
13584
|
+
if (parsed2.favoritesAgy) parsed2.modelCatalogScope = "agy";
|
|
12271
13585
|
return parsed2;
|
|
12272
13586
|
}
|
|
12273
13587
|
if (first === "providers") {
|
|
@@ -12308,6 +13622,16 @@ function parseArgs(args) {
|
|
|
12308
13622
|
parsed2.vertex = true;
|
|
12309
13623
|
continue;
|
|
12310
13624
|
}
|
|
13625
|
+
if (arg === "--with-native") {
|
|
13626
|
+
if (parsed2.codexLaunchMode === "relay-only") parsed2.error = "--with-native and --relay-only cannot be used together";
|
|
13627
|
+
parsed2.codexLaunchMode = "mixed";
|
|
13628
|
+
continue;
|
|
13629
|
+
}
|
|
13630
|
+
if (arg === "--relay-only") {
|
|
13631
|
+
if (parsed2.codexLaunchMode === "mixed") parsed2.error = "--with-native and --relay-only cannot be used together";
|
|
13632
|
+
parsed2.codexLaunchMode = "relay-only";
|
|
13633
|
+
continue;
|
|
13634
|
+
}
|
|
12311
13635
|
const consumed = tryConsumeRelayLaunchFlag(arg, rest, i, parsed2);
|
|
12312
13636
|
if (consumed !== null) {
|
|
12313
13637
|
if ("error" in consumed) return parsed2;
|
|
@@ -12356,6 +13680,16 @@ function parseArgs(args) {
|
|
|
12356
13680
|
parsed2.vertex = true;
|
|
12357
13681
|
continue;
|
|
12358
13682
|
}
|
|
13683
|
+
if (arg === "--with-native") {
|
|
13684
|
+
if (parsed2.codexLaunchMode === "relay-only") parsed2.error = "--with-native and --relay-only cannot be used together";
|
|
13685
|
+
parsed2.codexLaunchMode = "mixed";
|
|
13686
|
+
continue;
|
|
13687
|
+
}
|
|
13688
|
+
if (arg === "--relay-only") {
|
|
13689
|
+
if (parsed2.codexLaunchMode === "mixed") parsed2.error = "--with-native and --relay-only cannot be used together";
|
|
13690
|
+
parsed2.codexLaunchMode = "relay-only";
|
|
13691
|
+
continue;
|
|
13692
|
+
}
|
|
12359
13693
|
if (arg === "--help" || arg === "-h") {
|
|
12360
13694
|
parsed2.showHelp = true;
|
|
12361
13695
|
continue;
|
|
@@ -12511,6 +13845,7 @@ ${pc12.bold("Usage:")}
|
|
|
12511
13845
|
relay-ai ui
|
|
12512
13846
|
relay-ai models
|
|
12513
13847
|
relay-ai favorites
|
|
13848
|
+
relay-ai subagents
|
|
12514
13849
|
relay-ai providers
|
|
12515
13850
|
relay-ai --help
|
|
12516
13851
|
relay-ai --version
|
|
@@ -12529,6 +13864,7 @@ ${pc12.bold("Commands:")}
|
|
|
12529
13864
|
claude Launch Claude Code \u2014 pick a provider from your registry
|
|
12530
13865
|
models Manage favorite models for mid-session /model switching (max ${MAX_MODEL_CATALOG})
|
|
12531
13866
|
favorites Alias for models
|
|
13867
|
+
subagents Manage the independent Codex SubAgent model catalog (starts empty)
|
|
12532
13868
|
providers Add, import, and manage your AI providers
|
|
12533
13869
|
server Run a foreground API gateway (OpenCode Zen / Go and local providers)
|
|
12534
13870
|
codex Launch OpenAI Codex CLI with registry providers
|
|
@@ -12536,9 +13872,9 @@ ${pc12.bold("Commands:")}
|
|
|
12536
13872
|
agy Launch Antigravity CLI with registry providers
|
|
12537
13873
|
antigravity Launch Antigravity app with registry providers (macOS)
|
|
12538
13874
|
antigravity-ide Launch Antigravity IDE with registry providers (macOS)
|
|
12539
|
-
codex-app Launch ChatGPT desktop app (Codex mode) with registry providers (macOS + Windows)
|
|
13875
|
+
codex-app Launch ChatGPT desktop app (Codex mode) with registry providers (macOS + Windows + Linux)
|
|
12540
13876
|
chatgpt Alias for codex-app
|
|
12541
|
-
claude-app Launch Claude Desktop app with registry providers (macOS + Windows)
|
|
13877
|
+
claude-app Launch Claude Desktop app with registry providers (macOS + Windows + Linux)
|
|
12542
13878
|
|
|
12543
13879
|
${pc12.bold("Antigravity favorites:")}
|
|
12544
13880
|
agy, antigravity, and antigravity-ide share up to six Antigravity favorites
|
|
@@ -12668,7 +14004,22 @@ ${pc12.bold("Endpoints:")}
|
|
|
12668
14004
|
OpenAI-compatible: OPENAI_BASE_URL=http://127.0.0.1:17645/openai/v1
|
|
12669
14005
|
API key: use anything locally; use the server password in network mode.`;
|
|
12670
14006
|
}
|
|
12671
|
-
function modelsHelpText() {
|
|
14007
|
+
function modelsHelpText(scope = "global") {
|
|
14008
|
+
if (scope === "codex-subagents") {
|
|
14009
|
+
return `${pc12.bold("relay-ai subagents")} v${VERSION}
|
|
14010
|
+
Manage the separate Codex SubAgent model catalog.
|
|
14011
|
+
|
|
14012
|
+
${pc12.bold("Usage:")}
|
|
14013
|
+
relay-ai subagents
|
|
14014
|
+
relay-ai subagents --help
|
|
14015
|
+
relay-ai subagents --version
|
|
14016
|
+
|
|
14017
|
+
${pc12.bold("Behavior:")}
|
|
14018
|
+
Starts empty and is managed independently from General Favorites.
|
|
14019
|
+
Search all models at once or browse models by provider.
|
|
14020
|
+
Select one model that Codex uses for every SubAgent in mixed mode.
|
|
14021
|
+
The Codex SubAgent is saved to ~/.relay-ai/config.json (max ${CODEX_SUBAGENT_MODEL_CAP}).`;
|
|
14022
|
+
}
|
|
12672
14023
|
return `${pc12.bold("relay-ai favorites")} v${VERSION}
|
|
12673
14024
|
Manage favorite models for mid-session switching.
|
|
12674
14025
|
|
|
@@ -12678,6 +14029,7 @@ ${pc12.bold("Usage:")}
|
|
|
12678
14029
|
relay-ai models
|
|
12679
14030
|
relay-ai favorites --help
|
|
12680
14031
|
relay-ai favorites --version
|
|
14032
|
+
relay-ai subagents
|
|
12681
14033
|
|
|
12682
14034
|
${pc12.bold("Behavior:")}
|
|
12683
14035
|
Opens an interactive manager to add or remove favorites.
|
|
@@ -12685,9 +14037,11 @@ ${pc12.bold("Behavior:")}
|
|
|
12685
14037
|
Pick from Zen, Go, or any provider in your registry.
|
|
12686
14038
|
Global favorites are saved to ~/.relay-ai/config.json (max ${MAX_MODEL_CATALOG}).
|
|
12687
14039
|
--agy manages Antigravity CLI favorites only (max 6).
|
|
14040
|
+
relay-ai subagents manages the Codex SubAgent (starts empty; does not sync with General Favorites).
|
|
12688
14041
|
|
|
12689
14042
|
${pc12.bold("How it works:")}
|
|
12690
|
-
Claude/Codex/Gemini/server use the global favorites list.
|
|
14043
|
+
Claude/Codex/Gemini/server use the global favorites list. The Codex SubAgent is a
|
|
14044
|
+
separate model-only catalog used when Codex mixed mode is enabled.
|
|
12691
14045
|
Favorites appear in supported /model switch menus.
|
|
12692
14046
|
relay-ai agy, antigravity, and antigravity-ide use the Antigravity favorites
|
|
12693
14047
|
list so the limited native switch slots stay predictable: one selected launch
|
|
@@ -12784,9 +14138,9 @@ ${pc12.bold("Examples:")}
|
|
|
12784
14138
|
relay-ai antigravity
|
|
12785
14139
|
relay-ai antigravity --provider zen --model deepseek-v4-flash-free`;
|
|
12786
14140
|
}
|
|
12787
|
-
function printHelp(
|
|
14141
|
+
function printHelp(text5) {
|
|
12788
14142
|
console.log(`
|
|
12789
|
-
${
|
|
14143
|
+
${text5}
|
|
12790
14144
|
`);
|
|
12791
14145
|
}
|
|
12792
14146
|
async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindow, trace, claudeArgs) {
|
|
@@ -12823,15 +14177,19 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
|
|
|
12823
14177
|
var AGY_CLI_FAVORITES_CAP = 6;
|
|
12824
14178
|
async function runModelsCommand(opts = {}) {
|
|
12825
14179
|
const scope = opts.scope ?? "global";
|
|
12826
|
-
const maxFavorites = scope === "agy" ? AGY_CLI_FAVORITES_CAP : MAX_MODEL_CATALOG;
|
|
12827
|
-
const scopeName = scope === "agy" ? "Antigravity CLI Favorites" : "Favorite Models";
|
|
12828
|
-
const
|
|
14180
|
+
const maxFavorites = scope === "agy" ? AGY_CLI_FAVORITES_CAP : scope === "codex-subagents" ? CODEX_SUBAGENT_MODEL_CAP : MAX_MODEL_CATALOG;
|
|
14181
|
+
const scopeName = scope === "agy" ? "Antigravity CLI Favorites" : scope === "codex-subagents" ? "Codex SubAgent" : "Favorite Models";
|
|
14182
|
+
const subagentScope = scope === "codex-subagents";
|
|
14183
|
+
const listLabel = subagentScope ? "Codex SubAgent" : scope === "agy" ? "Antigravity Favorites" : "favorites";
|
|
14184
|
+
const listItemLabel = subagentScope ? "Codex SubAgent model" : scope === "agy" ? "Antigravity favorite" : "favorite";
|
|
14185
|
+
const configKey = scope === "agy" ? "antigravityCliFavoriteModels" : scope === "codex-subagents" ? "codexSubagentModels" : "favoriteModels";
|
|
12829
14186
|
relayIntro(scopeName);
|
|
12830
14187
|
const spinner9 = p14.spinner();
|
|
12831
14188
|
spinner9.start("Loading providers...");
|
|
12832
14189
|
const catalog = await fetchProviderCatalog();
|
|
12833
14190
|
spinner9.stop("");
|
|
12834
|
-
const
|
|
14191
|
+
const pickedProviders = providersForPicker(catalog);
|
|
14192
|
+
const allProviders = scope === "agy" ? providersForTarget(pickedProviders, "antigravity") : scope === "codex-subagents" ? providersForCodexSubagents(pickedProviders) : pickedProviders;
|
|
12835
14193
|
const favoriteProviders = allProviders.map((provider) => ({
|
|
12836
14194
|
...provider,
|
|
12837
14195
|
name: favoriteProviderDisplayName(provider)
|
|
@@ -12849,7 +14207,7 @@ async function runModelsCommand(opts = {}) {
|
|
|
12849
14207
|
}
|
|
12850
14208
|
}
|
|
12851
14209
|
const prefs = loadPreferences();
|
|
12852
|
-
let favorites = scope === "agy" ? prefs.antigravityCliFavoriteModels ?? [] : prefs.favoriteModels ?? [];
|
|
14210
|
+
let favorites = scope === "agy" ? prefs.antigravityCliFavoriteModels ?? [] : scope === "codex-subagents" ? prefs.codexSubagentModels ?? [] : prefs.favoriteModels ?? [];
|
|
12853
14211
|
let favoritesDirty = false;
|
|
12854
14212
|
while (true) {
|
|
12855
14213
|
const options = [];
|
|
@@ -12863,7 +14221,7 @@ async function runModelsCommand(opts = {}) {
|
|
|
12863
14221
|
options.push({
|
|
12864
14222
|
value: "__add__",
|
|
12865
14223
|
label: atCap ? pc12.dim(`+ Add a model \u2192 (limit of ${maxFavorites} reached)`) : pc12.cyan("+ Add a model \u2192"),
|
|
12866
|
-
hint: atCap ?
|
|
14224
|
+
hint: atCap ? `Remove a ${listItemLabel} first to make room` : `${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""} available`
|
|
12867
14225
|
});
|
|
12868
14226
|
options.push({ value: "__done__", label: "Done", hint: "" });
|
|
12869
14227
|
const header = favorites.length === 0 ? `${scopeName} (0/${maxFavorites})` : `${scopeName} (${favorites.length}/${maxFavorites}) \u2014 select to remove`;
|
|
@@ -12875,16 +14233,16 @@ async function runModelsCommand(opts = {}) {
|
|
|
12875
14233
|
if (p14.isCancel(choice) || choice === "__done__") break;
|
|
12876
14234
|
if (choice === "__add__") {
|
|
12877
14235
|
if (atCap) {
|
|
12878
|
-
p14.log.warn(`Limit of ${maxFavorites} favorites reached \u2014 remove one first.`);
|
|
14236
|
+
p14.log.warn(`Limit of ${maxFavorites} ${subagentScope ? "Codex SubAgent" : "favorites"} reached \u2014 remove one first.`);
|
|
12879
14237
|
continue;
|
|
12880
14238
|
}
|
|
12881
14239
|
const globalCount = buildGlobalFavoriteIndex(favoriteProviders).length;
|
|
12882
14240
|
const addPath = await p14.select({
|
|
12883
|
-
message: "Add a favorite",
|
|
14241
|
+
message: subagentScope ? "Add a Codex SubAgent model" : "Add a favorite",
|
|
12884
14242
|
options: [
|
|
12885
14243
|
{
|
|
12886
14244
|
value: "global",
|
|
12887
|
-
label: pc12.cyan("Search all providers"),
|
|
14245
|
+
label: pc12.cyan(subagentScope ? "Search all models" : "Search all providers"),
|
|
12888
14246
|
hint: `${globalCount} models \xB7 ${favoriteProviders.length} provider${favoriteProviders.length !== 1 ? "s" : ""}`
|
|
12889
14247
|
},
|
|
12890
14248
|
{
|
|
@@ -12903,7 +14261,7 @@ async function runModelsCommand(opts = {}) {
|
|
|
12903
14261
|
let provider;
|
|
12904
14262
|
let browsedMultiple = [];
|
|
12905
14263
|
if (addPath === "global") {
|
|
12906
|
-
const globalPick = await pickGlobalFavoriteModel(favoriteProviders, favorites);
|
|
14264
|
+
const globalPick = await pickGlobalFavoriteModel(favoriteProviders, favorites, { listLabel });
|
|
12907
14265
|
if (globalPick === null) continue;
|
|
12908
14266
|
if (globalPick !== ADD_BY_PROVIDER) {
|
|
12909
14267
|
provider = favoriteProviders.find((ap) => ap.id === globalPick.providerId);
|
|
@@ -12911,7 +14269,7 @@ async function runModelsCommand(opts = {}) {
|
|
|
12911
14269
|
}
|
|
12912
14270
|
}
|
|
12913
14271
|
if (addPath === "free") {
|
|
12914
|
-
const globalPick = await pickGlobalFavoriteModel(favoriteProviders, favorites, { freeOnly: true });
|
|
14272
|
+
const globalPick = await pickGlobalFavoriteModel(favoriteProviders, favorites, { freeOnly: true, listLabel });
|
|
12915
14273
|
if (globalPick === null) continue;
|
|
12916
14274
|
if (globalPick !== ADD_BY_PROVIDER) {
|
|
12917
14275
|
provider = favoriteProviders.find((ap) => ap.id === globalPick.providerId);
|
|
@@ -12929,13 +14287,30 @@ async function runModelsCommand(opts = {}) {
|
|
|
12929
14287
|
});
|
|
12930
14288
|
if (p14.isCancel(pickedProviderId)) break;
|
|
12931
14289
|
provider = favoriteProviders.find((ap) => ap.id === pickedProviderId);
|
|
12932
|
-
|
|
14290
|
+
let modelsToPick = provider.models;
|
|
14291
|
+
if (provider.models.length > MODEL_SEARCH_THRESHOLD) {
|
|
14292
|
+
const searchInput = await p14.text({
|
|
14293
|
+
message: `Search ${provider.name} models (${provider.models.length} available):`,
|
|
14294
|
+
placeholder: "e.g. flash 3.6, claude, llama"
|
|
14295
|
+
});
|
|
14296
|
+
if (p14.isCancel(searchInput)) {
|
|
14297
|
+
currentInitialProvider = provider.id;
|
|
14298
|
+
continue;
|
|
14299
|
+
}
|
|
14300
|
+
modelsToPick = filterModelsBySearch(provider.models, String(searchInput));
|
|
14301
|
+
if (modelsToPick.length === 0) {
|
|
14302
|
+
p14.log.warn("No models match \u2014 try a different search");
|
|
14303
|
+
currentInitialProvider = provider.id;
|
|
14304
|
+
continue;
|
|
14305
|
+
}
|
|
14306
|
+
}
|
|
14307
|
+
const options2 = modelsToPick.map((m) => {
|
|
12933
14308
|
const favorited = isFavorite(favorites, { providerId: provider.id, modelId: m.id });
|
|
12934
14309
|
const label = formatCodexModelLabel(m);
|
|
12935
14310
|
return {
|
|
12936
14311
|
value: m.id,
|
|
12937
14312
|
label: fmtModel(label, m.id),
|
|
12938
|
-
hint: favorited ? pc12.yellow(
|
|
14313
|
+
hint: favorited ? pc12.yellow(`\u2605 already in ${listLabel}`) : ""
|
|
12939
14314
|
};
|
|
12940
14315
|
});
|
|
12941
14316
|
const pickedModelIds = await p14.multiselect({
|
|
@@ -12951,7 +14326,7 @@ async function runModelsCommand(opts = {}) {
|
|
|
12951
14326
|
currentInitialProvider = provider.id;
|
|
12952
14327
|
continue;
|
|
12953
14328
|
}
|
|
12954
|
-
browsedMultiple =
|
|
14329
|
+
browsedMultiple = modelsToPick.filter((m) => pickedModelIds.includes(m.id));
|
|
12955
14330
|
break;
|
|
12956
14331
|
}
|
|
12957
14332
|
if (browsedMultiple.length === 0) continue;
|
|
@@ -12978,36 +14353,36 @@ async function runModelsCommand(opts = {}) {
|
|
|
12978
14353
|
if (addedModels.length > 0) {
|
|
12979
14354
|
if (addedModels.length === 1) {
|
|
12980
14355
|
const modelName = addedModels[0].name || addedModels[0].id;
|
|
12981
|
-
p14.log.success(`Added ${modelName} (${provider.name}) to
|
|
14356
|
+
p14.log.success(`Added ${modelName} (${provider.name}) to ${listLabel}.`);
|
|
12982
14357
|
} else {
|
|
12983
|
-
p14.log.success(`Added ${addedModels.length} models from ${provider.name} to
|
|
14358
|
+
p14.log.success(`Added ${addedModels.length} models from ${provider.name} to ${listLabel}.`);
|
|
12984
14359
|
}
|
|
12985
14360
|
}
|
|
12986
14361
|
if (duplicateCount > 0) {
|
|
12987
|
-
p14.log.warn(`${duplicateCount} selected model(s) were already in
|
|
14362
|
+
p14.log.warn(`${duplicateCount} selected model(s) were already in ${listLabel}.`);
|
|
12988
14363
|
}
|
|
12989
14364
|
if (limitReached) {
|
|
12990
|
-
p14.log.warn(`Limit of ${maxFavorites} favorites reached \u2014 some selected models could not be added.`);
|
|
14365
|
+
p14.log.warn(`Limit of ${maxFavorites} ${subagentScope ? "Codex SubAgent" : "favorites"} reached \u2014 some selected models could not be added.`);
|
|
12991
14366
|
}
|
|
12992
14367
|
} else if (choice.startsWith("fav-")) {
|
|
12993
14368
|
const idx = parseInt(choice.slice(4), 10);
|
|
12994
14369
|
const fav = favorites[idx];
|
|
12995
14370
|
const entry = modelLookup.get(`${fav.providerId}:${fav.modelId}`);
|
|
12996
14371
|
const label = entry ? `${entry.modelName} (${entry.providerName})` : fav.modelId;
|
|
12997
|
-
const confirmed = await p14.confirm({ message: `Remove ${label} from
|
|
14372
|
+
const confirmed = await p14.confirm({ message: `Remove ${label} from ${listLabel}?` });
|
|
12998
14373
|
if (p14.isCancel(confirmed) || !confirmed) continue;
|
|
12999
14374
|
favorites = removeFavorite(favorites, fav);
|
|
13000
14375
|
favoritesDirty = true;
|
|
13001
|
-
p14.log.success(`Removed ${label} from
|
|
14376
|
+
p14.log.success(`Removed ${label} from ${listLabel}.`);
|
|
13002
14377
|
}
|
|
13003
14378
|
}
|
|
13004
14379
|
if (favoritesDirty) {
|
|
13005
14380
|
savePreferences({ [configKey]: favorites });
|
|
13006
14381
|
}
|
|
13007
|
-
const
|
|
14382
|
+
const summary = subagentScope ? favorites.length === 0 ? "No Codex SubAgent configured" : `${favorites.length} Codex SubAgent model${favorites.length !== 1 ? "s" : ""} saved` : favorites.length === 0 ? `No ${scope === "agy" ? "Antigravity CLI favorites" : "favorites"} saved` : `${favorites.length} ${scope === "agy" ? "Antigravity CLI favorite" : "favorite"}${favorites.length !== 1 ? "s" : ""} saved`;
|
|
13008
14383
|
relayOutro(
|
|
13009
|
-
|
|
13010
|
-
favorites.length === 0 ? pc12.dim("Launch uses single-model mode") : pc12.cyan("/model menu ready on next launch")
|
|
14384
|
+
summary,
|
|
14385
|
+
favorites.length === 0 ? pc12.dim("Launch uses single-model mode") : subagentScope ? pc12.cyan("Codex will use this model for every Relay SubAgent") : pc12.cyan("/model menu ready on next launch")
|
|
13011
14386
|
);
|
|
13012
14387
|
return 0;
|
|
13013
14388
|
}
|
|
@@ -13384,7 +14759,9 @@ Error: ${launchPlan.error}
|
|
|
13384
14759
|
reasoning: selectedModel.reasoning,
|
|
13385
14760
|
interleavedReasoningField: selectedModel.interleavedReasoningField,
|
|
13386
14761
|
useResponsesLite: selectedModel.useResponsesLite,
|
|
13387
|
-
preferWebSockets: selectedModel.preferWebSockets
|
|
14762
|
+
preferWebSockets: selectedModel.preferWebSockets,
|
|
14763
|
+
refreshToken: providerRefreshToken(activeProvider.id, activeProvider.authType, activeProvider.authRef),
|
|
14764
|
+
headers: activeProvider.headers
|
|
13388
14765
|
},
|
|
13389
14766
|
launchApiKey
|
|
13390
14767
|
);
|
|
@@ -13492,7 +14869,7 @@ Options:
|
|
|
13492
14869
|
--trace Write debug logs under ~/.relay-ai/logs/`);
|
|
13493
14870
|
return 0;
|
|
13494
14871
|
}
|
|
13495
|
-
const { runUiCommand } = await import("./ui-command-
|
|
14872
|
+
const { runUiCommand } = await import("./ui-command-FARF2BF4.js");
|
|
13496
14873
|
return runUiCommand({ trace: parsed.trace, serverMode: parsed.uiServerMode });
|
|
13497
14874
|
}
|
|
13498
14875
|
if (parsed.command === "models") {
|
|
@@ -13501,10 +14878,10 @@ Options:
|
|
|
13501
14878
|
return 0;
|
|
13502
14879
|
}
|
|
13503
14880
|
if (parsed.showHelp) {
|
|
13504
|
-
printHelp(modelsHelpText());
|
|
14881
|
+
printHelp(modelsHelpText(parsed.modelCatalogScope === "codex-subagents" ? "codex-subagents" : "global"));
|
|
13505
14882
|
return 0;
|
|
13506
14883
|
}
|
|
13507
|
-
return runModelsCommand({ scope: parsed.favoritesAgy ? "agy" : "global" });
|
|
14884
|
+
return runModelsCommand({ scope: parsed.modelCatalogScope ?? (parsed.favoritesAgy ? "agy" : "global") });
|
|
13508
14885
|
}
|
|
13509
14886
|
if (parsed.command === "providers") {
|
|
13510
14887
|
if (parsed.showVersion) {
|
|
@@ -13517,6 +14894,12 @@ Options:
|
|
|
13517
14894
|
}
|
|
13518
14895
|
if (parsed.trace) {
|
|
13519
14896
|
process.env.RELAY_AI_TRACE = "1";
|
|
14897
|
+
const providerTraceLogPath = prepareProviderTraceLog();
|
|
14898
|
+
try {
|
|
14899
|
+
return await runProvidersCommand(parsed.claudeArgs);
|
|
14900
|
+
} finally {
|
|
14901
|
+
printTraceLog(providerTraceLogPath);
|
|
14902
|
+
}
|
|
13520
14903
|
}
|
|
13521
14904
|
return runProvidersCommand(parsed.claudeArgs);
|
|
13522
14905
|
}
|
|
@@ -13529,7 +14912,7 @@ Options:
|
|
|
13529
14912
|
console.log(codexAppHelpText());
|
|
13530
14913
|
return 0;
|
|
13531
14914
|
}
|
|
13532
|
-
return runCodexAppCommand(parsed.claudeArgs, { vertex: parsed.vertex, launchProvider: parsed.launchProvider, launchModel: parsed.launchModel });
|
|
14915
|
+
return runCodexAppCommand(parsed.claudeArgs, { vertex: parsed.vertex, launchProvider: parsed.launchProvider, launchModel: parsed.launchModel, codexLaunchMode: parsed.codexLaunchMode });
|
|
13533
14916
|
}
|
|
13534
14917
|
if (parsed.command === "claude-app") {
|
|
13535
14918
|
if (parsed.showVersion) {
|
|
@@ -13554,7 +14937,8 @@ Options:
|
|
|
13554
14937
|
return runCodexCommand(parsed.claudeArgs, parsed.trace, {
|
|
13555
14938
|
launchProvider: parsed.launchProvider,
|
|
13556
14939
|
launchModel: parsed.launchModel,
|
|
13557
|
-
vertex: parsed.vertex
|
|
14940
|
+
vertex: parsed.vertex,
|
|
14941
|
+
codexLaunchMode: parsed.codexLaunchMode
|
|
13558
14942
|
});
|
|
13559
14943
|
}
|
|
13560
14944
|
if (parsed.command === "gemini") {
|