@jacobbd/relay-ai 0.8.0 → 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 +18 -6
- package/dist/{chunk-5DDQJSTU.js → chunk-R4AWEK7T.js} +392 -117
- package/dist/chunk-R4AWEK7T.js.map +1 -0
- package/dist/cli.js +1618 -341
- package/dist/cli.js.map +1 -1
- package/dist/core/index.js +93 -79
- package/dist/core/index.js.map +1 -1
- package/dist/ui/public/app.js +164 -41
- package/dist/ui/public/index.html +32 -0
- package/dist/ui/public/provider-model-browser.js +18 -2
- package/dist/{ui-command-RYWL36VR.js → ui-command-FARF2BF4.js} +54 -10
- package/dist/ui-command-FARF2BF4.js.map +1 -0
- package/package.json +4 -3
- package/dist/chunk-5DDQJSTU.js.map +0 -1
- package/dist/ui-command-RYWL36VR.js.map +0 -1
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,6 +122,7 @@ import {
|
|
|
115
122
|
parseCodexAppModelSlug,
|
|
116
123
|
parseDsmlToolCalls,
|
|
117
124
|
parseToolArguments,
|
|
125
|
+
preferredRelayCredentialAuthRef,
|
|
118
126
|
prepareClaudeTraceLog,
|
|
119
127
|
prepareProviderTraceLog,
|
|
120
128
|
printApiKeyPanel,
|
|
@@ -129,6 +137,7 @@ import {
|
|
|
129
137
|
providerAuthHelpText,
|
|
130
138
|
providerRefreshToken,
|
|
131
139
|
providerSelectOption,
|
|
140
|
+
providersForCodexSubagents,
|
|
132
141
|
providersForPicker,
|
|
133
142
|
providersForTarget,
|
|
134
143
|
quitClaudeAppGracefully,
|
|
@@ -137,13 +146,16 @@ import {
|
|
|
137
146
|
readFromCredentialStore,
|
|
138
147
|
readGlobalOpencodeCredential,
|
|
139
148
|
readOpencodeAuthFile,
|
|
149
|
+
readStoredProviderCredential,
|
|
140
150
|
recordLaunchSelection,
|
|
141
151
|
refreshAllProviderModels,
|
|
142
152
|
refreshModelsDevCacheAsync,
|
|
143
153
|
refreshProviderModels,
|
|
144
154
|
relayIntro,
|
|
145
155
|
relayOutro,
|
|
156
|
+
removeFavorite,
|
|
146
157
|
removeProviderFromRegistry,
|
|
158
|
+
renderMultiAgentV2Feature,
|
|
147
159
|
resetCodexBodyDumpLog,
|
|
148
160
|
resolveApiKey,
|
|
149
161
|
resolveContextWindow,
|
|
@@ -175,6 +187,7 @@ import {
|
|
|
175
187
|
startServer,
|
|
176
188
|
summarizeSdkRequestForTrace,
|
|
177
189
|
supportsClaudeTransparentMode,
|
|
190
|
+
supportsMultiAgentV2,
|
|
178
191
|
supportsNativeOAuth,
|
|
179
192
|
syntheticTemplate,
|
|
180
193
|
thinkingProviderOptions,
|
|
@@ -184,7 +197,7 @@ import {
|
|
|
184
197
|
validateCustomEndpointUrl,
|
|
185
198
|
writeSecureLogLine,
|
|
186
199
|
zenRegistryStub
|
|
187
|
-
} from "./chunk-
|
|
200
|
+
} from "./chunk-R4AWEK7T.js";
|
|
188
201
|
import {
|
|
189
202
|
filterTemplates,
|
|
190
203
|
getTemplateById,
|
|
@@ -1105,19 +1118,6 @@ async function pickLocalModel(provider, conflicts, prefs) {
|
|
|
1105
1118
|
return selectedModel;
|
|
1106
1119
|
}
|
|
1107
1120
|
|
|
1108
|
-
// src/favorites.ts
|
|
1109
|
-
function isFavorite(list, fav) {
|
|
1110
|
-
return list.some((f) => f.providerId === fav.providerId && f.modelId === fav.modelId);
|
|
1111
|
-
}
|
|
1112
|
-
function addFavorite(list, fav, max = MAX_MODEL_CATALOG) {
|
|
1113
|
-
if (isFavorite(list, fav)) return { ok: false, reason: "duplicate" };
|
|
1114
|
-
if (list.length >= max) return { ok: false, reason: "cap" };
|
|
1115
|
-
return { ok: true, list: [...list, fav] };
|
|
1116
|
-
}
|
|
1117
|
-
function removeFavorite(list, fav) {
|
|
1118
|
-
return list.filter((f) => !(f.providerId === fav.providerId && f.modelId === fav.modelId));
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
1121
|
// src/favorites-picker.ts
|
|
1122
1122
|
import * as p4 from "@clack/prompts";
|
|
1123
1123
|
import pc3 from "picocolors";
|
|
@@ -1164,14 +1164,15 @@ function filterGlobalFavoriteIndex(entries, query, opts) {
|
|
|
1164
1164
|
return a.index - b.index;
|
|
1165
1165
|
}).map((result) => result.entry);
|
|
1166
1166
|
}
|
|
1167
|
-
function globalFavoriteSelectOption(entry, favorites) {
|
|
1167
|
+
function globalFavoriteSelectOption(entry, favorites, opts) {
|
|
1168
1168
|
const label = formatCodexModelLabel(entry.model);
|
|
1169
1169
|
const favorited = isFavorite(favorites, { providerId: entry.providerId, modelId: entry.model.id });
|
|
1170
1170
|
const providerTag = fmtProviderBracket(entry.providerId, entry.providerName, entry.model.isFree);
|
|
1171
|
+
const listLabel = opts?.listLabel ?? "favorites";
|
|
1171
1172
|
return {
|
|
1172
1173
|
value: globalFavoritePickKey(entry),
|
|
1173
1174
|
label: `${fmtModel(label, entry.model.id)} ${providerTag}`,
|
|
1174
|
-
hint: favorited ? pc3.dim(
|
|
1175
|
+
hint: favorited ? pc3.dim(`already in ${listLabel}`) : ""
|
|
1175
1176
|
};
|
|
1176
1177
|
}
|
|
1177
1178
|
function parseGlobalFavoritePickKey(key, index) {
|
|
@@ -1181,16 +1182,18 @@ async function pickGlobalFavoriteModel(providers, favorites, opts) {
|
|
|
1181
1182
|
const index = buildGlobalFavoriteIndex(providers);
|
|
1182
1183
|
if (index.length === 0) return null;
|
|
1183
1184
|
const freeOnly = opts?.freeOnly === true;
|
|
1185
|
+
const listLabel = opts?.listLabel ?? "favorites";
|
|
1186
|
+
const subagents = listLabel === "Codex Sub-agents";
|
|
1184
1187
|
while (true) {
|
|
1185
1188
|
const searchInput = await p4.text({
|
|
1186
|
-
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):`,
|
|
1187
1190
|
placeholder: "e.g. deepseek, claude, sonnet"
|
|
1188
1191
|
});
|
|
1189
1192
|
if (p4.isCancel(searchInput)) {
|
|
1190
1193
|
const fallback = await p4.select({
|
|
1191
|
-
message: "Add a favorite",
|
|
1194
|
+
message: subagents ? "Add a Codex Sub-agent model" : "Add a favorite",
|
|
1192
1195
|
options: [
|
|
1193
|
-
{ 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: "" },
|
|
1194
1197
|
{ value: ADD_BY_PROVIDER, label: pc3.cyan("Browse by provider \u2192"), hint: "Pick one provider first" }
|
|
1195
1198
|
]
|
|
1196
1199
|
});
|
|
@@ -1207,7 +1210,8 @@ async function pickGlobalFavoriteModel(providers, favorites, opts) {
|
|
|
1207
1210
|
matched.map((e) => ({ ...e, id: globalFavoritePickKey(e) })),
|
|
1208
1211
|
(e) => globalFavoriteSelectOption(
|
|
1209
1212
|
{ providerId: e.providerId, providerName: e.providerName, model: e.model },
|
|
1210
|
-
favorites
|
|
1213
|
+
favorites,
|
|
1214
|
+
{ listLabel }
|
|
1211
1215
|
),
|
|
1212
1216
|
matched.length === 1 ? "Match found" : `Select model (${matched.length} matches)`,
|
|
1213
1217
|
void 0,
|
|
@@ -1218,7 +1222,7 @@ async function pickGlobalFavoriteModel(providers, favorites, opts) {
|
|
|
1218
1222
|
const picked = parseGlobalFavoritePickKey(result.id, matched);
|
|
1219
1223
|
if (!picked) continue;
|
|
1220
1224
|
if (isFavorite(favorites, { providerId: picked.providerId, modelId: picked.model.id })) {
|
|
1221
|
-
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}.`);
|
|
1222
1226
|
continue;
|
|
1223
1227
|
}
|
|
1224
1228
|
return picked;
|
|
@@ -1887,6 +1891,13 @@ async function runProviderDetail(id) {
|
|
|
1887
1891
|
hint: "Refresh OAuth tokens or switch accounts"
|
|
1888
1892
|
});
|
|
1889
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
|
+
}
|
|
1890
1901
|
detailOptions.push(
|
|
1891
1902
|
{
|
|
1892
1903
|
value: "toggle",
|
|
@@ -1916,6 +1927,9 @@ async function runProviderDetail(id) {
|
|
|
1916
1927
|
if (action === "refresh") {
|
|
1917
1928
|
return await runProvidersRefreshModels(id) === 0 ? "back" : "failed";
|
|
1918
1929
|
}
|
|
1930
|
+
if (action === "change-key") {
|
|
1931
|
+
return await runProviderApiKeyChange(provider, registry) === 0 ? "back" : "failed";
|
|
1932
|
+
}
|
|
1919
1933
|
if (action === "change-auth" && template) {
|
|
1920
1934
|
return await runDualAuthTemplateFlow(template, provider) === 0 ? "back" : "failed";
|
|
1921
1935
|
}
|
|
@@ -1932,6 +1946,51 @@ async function runProviderDetail(id) {
|
|
|
1932
1946
|
const code = await runProvidersRemove(id, true);
|
|
1933
1947
|
return code === 0 ? "removed" : "failed";
|
|
1934
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;
|
|
1993
|
+
}
|
|
1935
1994
|
async function runProvidersHub() {
|
|
1936
1995
|
const hasOpencode = findOpencodeBinary() !== null;
|
|
1937
1996
|
let lastOperationFailed = false;
|
|
@@ -2038,10 +2097,13 @@ async function runProvidersCommand(args) {
|
|
|
2038
2097
|
// src/codex.ts
|
|
2039
2098
|
import pc7 from "picocolors";
|
|
2040
2099
|
import * as p8 from "@clack/prompts";
|
|
2100
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2101
|
+
import { join as join5 } from "path";
|
|
2041
2102
|
|
|
2042
2103
|
// src/codex-proxy.ts
|
|
2043
|
-
import { createHash } from "crypto";
|
|
2104
|
+
import { createHash as createHash2 } from "crypto";
|
|
2044
2105
|
import { createServer } from "http";
|
|
2106
|
+
import { WebSocket } from "ws";
|
|
2045
2107
|
|
|
2046
2108
|
// src/oauth/claude-code-identity.ts
|
|
2047
2109
|
function isClaudeCodeOAuthRoute(input) {
|
|
@@ -2147,8 +2209,8 @@ function extractDeveloperAndInstructions(items, instructions) {
|
|
|
2147
2209
|
const remaining = [];
|
|
2148
2210
|
for (const item of items) {
|
|
2149
2211
|
if ("role" in item && item.role === "developer") {
|
|
2150
|
-
const
|
|
2151
|
-
if (
|
|
2212
|
+
const text5 = messageText(item.content);
|
|
2213
|
+
if (text5.trim()) developerParts.push(text5.trim());
|
|
2152
2214
|
} else {
|
|
2153
2215
|
remaining.push(item);
|
|
2154
2216
|
}
|
|
@@ -2221,11 +2283,11 @@ function ensureUserFirst(messages) {
|
|
|
2221
2283
|
function reasoningSummaryText(item) {
|
|
2222
2284
|
return (item.summary ?? []).map((part) => part.type === "summary_text" ? part.text ?? "" : "").join("");
|
|
2223
2285
|
}
|
|
2224
|
-
function makeReasoningOutputItem(id,
|
|
2286
|
+
function makeReasoningOutputItem(id, text5) {
|
|
2225
2287
|
return {
|
|
2226
2288
|
id,
|
|
2227
2289
|
type: "reasoning",
|
|
2228
|
-
summary:
|
|
2290
|
+
summary: text5.trim() ? [{ type: "summary_text", text: text5 }] : []
|
|
2229
2291
|
};
|
|
2230
2292
|
}
|
|
2231
2293
|
function translateResponsesInput(input, instructions, npm, toolContext = createCodexToolContext()) {
|
|
@@ -2325,6 +2387,11 @@ function translateResponsesInput(input, instructions, npm, toolContext = createC
|
|
|
2325
2387
|
output: { type: "text", value: serializeToolResultContent(item.output) }
|
|
2326
2388
|
}]
|
|
2327
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
|
+
}
|
|
2328
2395
|
} else if (item.type === "compaction" || item.type === "context_compaction") {
|
|
2329
2396
|
const summary = decodeCompactionContent(item.encrypted_content) ?? "";
|
|
2330
2397
|
if (summary.trim()) {
|
|
@@ -2333,8 +2400,8 @@ ${summary}` }] });
|
|
|
2333
2400
|
}
|
|
2334
2401
|
} else if ("role" in item) {
|
|
2335
2402
|
const role = item.role === "assistant" ? "assistant" : "user";
|
|
2336
|
-
const
|
|
2337
|
-
messages.push({ role, content: [{ type: "text", text:
|
|
2403
|
+
const text5 = messageText(item.content);
|
|
2404
|
+
messages.push({ role, content: [{ type: "text", text: text5 }] });
|
|
2338
2405
|
}
|
|
2339
2406
|
}
|
|
2340
2407
|
return {
|
|
@@ -2435,6 +2502,27 @@ function usageFromPart(part) {
|
|
|
2435
2502
|
const output = part.totalUsage?.outputTokens ?? 0;
|
|
2436
2503
|
return { input_tokens: input, output_tokens: output, total_tokens: input + output };
|
|
2437
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
|
+
}
|
|
2438
2526
|
function resolveOutputKind(flatName, ctx) {
|
|
2439
2527
|
if (!ctx) return { kind: "plain" };
|
|
2440
2528
|
if (flatName === TOOL_SEARCH_NAME) return { kind: "tool_search" };
|
|
@@ -2444,6 +2532,7 @@ function resolveOutputKind(flatName, ctx) {
|
|
|
2444
2532
|
return { kind: "plain" };
|
|
2445
2533
|
}
|
|
2446
2534
|
function buildFinalToolItem(kind, flatName, callId, itemId, argsStr) {
|
|
2535
|
+
argsStr = normalizeCodexSubagentArguments(flatName, argsStr);
|
|
2447
2536
|
switch (kind.kind) {
|
|
2448
2537
|
case "namespace":
|
|
2449
2538
|
return { type: "function_call", id: itemId, call_id: callId, namespace: kind.namespace, name: kind.name, arguments: argsStr, status: "completed" };
|
|
@@ -2815,13 +2904,14 @@ async function writeResponsesStream(fullStream, modelId, write, onDone, onProgre
|
|
|
2815
2904
|
outputItems.unshift(reasoningItem);
|
|
2816
2905
|
}
|
|
2817
2906
|
for (const tool3 of toolStates) {
|
|
2907
|
+
const normalizedArgs = normalizeCodexSubagentArguments(tool3.name, tool3.args);
|
|
2818
2908
|
emit("response.function_call_arguments.done", {
|
|
2819
2909
|
type: "response.function_call_arguments.done",
|
|
2820
2910
|
item_id: tool3.itemId,
|
|
2821
2911
|
output_index: tool3.outputIndex,
|
|
2822
|
-
arguments:
|
|
2912
|
+
arguments: normalizedArgs
|
|
2823
2913
|
});
|
|
2824
|
-
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);
|
|
2825
2915
|
emit("response.output_item.done", {
|
|
2826
2916
|
type: "response.output_item.done",
|
|
2827
2917
|
output_index: tool3.outputIndex,
|
|
@@ -3104,6 +3194,412 @@ function responsesRateLimitBody(modelId, message) {
|
|
|
3104
3194
|
};
|
|
3105
3195
|
}
|
|
3106
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
|
+
|
|
3107
3603
|
// src/codex-proxy.ts
|
|
3108
3604
|
function captureCompletedResponse(sseText) {
|
|
3109
3605
|
if (!sseText.includes("response.completed")) return void 0;
|
|
@@ -3135,15 +3631,15 @@ function estimateCodexRequestChars(params) {
|
|
|
3135
3631
|
}
|
|
3136
3632
|
return chars;
|
|
3137
3633
|
}
|
|
3138
|
-
function clipTextForContext(
|
|
3139
|
-
if (
|
|
3634
|
+
function clipTextForContext(text5, maxChars) {
|
|
3635
|
+
if (text5.length <= maxChars) return text5;
|
|
3140
3636
|
const marker = `
|
|
3141
3637
|
|
|
3142
|
-
[... ${
|
|
3638
|
+
[... ${text5.length} chars clipped from oversized context item ...]
|
|
3143
3639
|
|
|
3144
3640
|
`;
|
|
3145
3641
|
const edge = Math.max(1, Math.floor((maxChars - marker.length) / 2));
|
|
3146
|
-
return `${
|
|
3642
|
+
return `${text5.slice(0, edge)}${marker}${text5.slice(-edge)}`;
|
|
3147
3643
|
}
|
|
3148
3644
|
function clipLargeTextParts(params, maxCharsPerPart) {
|
|
3149
3645
|
const messages = params.messages.map((msg) => {
|
|
@@ -3227,6 +3723,7 @@ function protectCodexCompactionParams(body, params, contextWindow) {
|
|
|
3227
3723
|
};
|
|
3228
3724
|
}
|
|
3229
3725
|
var PROXY_PLACEHOLDER_KEY = "proxy-local";
|
|
3726
|
+
var MAX_CODEX_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
3230
3727
|
function codexRouteLookupIds(requestedModel) {
|
|
3231
3728
|
const ids = routeLookupIds(requestedModel);
|
|
3232
3729
|
const bare = parseCodexAppModelSlug(requestedModel);
|
|
@@ -3255,6 +3752,27 @@ function findCodexProxyRoute(routes, requestedModel) {
|
|
|
3255
3752
|
}
|
|
3256
3753
|
return void 0;
|
|
3257
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
|
+
}
|
|
3258
3776
|
function resolveModel(routes, models, requestedModel) {
|
|
3259
3777
|
const route = findCodexProxyRoute(routes, requestedModel);
|
|
3260
3778
|
if (!route) return void 0;
|
|
@@ -3262,10 +3780,30 @@ function resolveModel(routes, models, requestedModel) {
|
|
|
3262
3780
|
if (!languageModel) return void 0;
|
|
3263
3781
|
return { route, languageModel };
|
|
3264
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
|
+
}
|
|
3265
3801
|
async function startCodexProxy(routes, options = {}) {
|
|
3266
3802
|
const opts = typeof options === "boolean" ? { debug: options } : options;
|
|
3267
3803
|
const debug = opts.debug ?? false;
|
|
3268
3804
|
const requireAuth = opts.requireAuth ?? true;
|
|
3805
|
+
const mixedNative = opts.mixedNative;
|
|
3806
|
+
const nativePayloadRelay = mixedNative ? createNativePayloadRelay({}) : void 0;
|
|
3269
3807
|
silenceSdkWarnings();
|
|
3270
3808
|
const models = /* @__PURE__ */ new Map();
|
|
3271
3809
|
for (const route of routes) {
|
|
@@ -3296,6 +3834,17 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3296
3834
|
process.on("unhandledRejection", onRejection);
|
|
3297
3835
|
const server = createServer(async (req, res) => {
|
|
3298
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;
|
|
3299
3848
|
if (debug) {
|
|
3300
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)"}`);
|
|
3301
3850
|
}
|
|
@@ -3317,11 +3866,11 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3317
3866
|
return;
|
|
3318
3867
|
}
|
|
3319
3868
|
}
|
|
3320
|
-
if (req.method === "GET" &&
|
|
3869
|
+
if (req.method === "GET" && effectivePath === "/health") {
|
|
3321
3870
|
sendJson(res, 200, { ok: true });
|
|
3322
3871
|
return;
|
|
3323
3872
|
}
|
|
3324
|
-
if (req.method === "GET" &&
|
|
3873
|
+
if (req.method === "GET" && effectivePath === "/v1/models") {
|
|
3325
3874
|
const data = [];
|
|
3326
3875
|
const seenIds = /* @__PURE__ */ new Set();
|
|
3327
3876
|
const addModel = (id, providerId) => {
|
|
@@ -3341,14 +3890,21 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3341
3890
|
addModel(`${route.providerId}__${route.modelId}`, route.providerId);
|
|
3342
3891
|
}
|
|
3343
3892
|
}
|
|
3893
|
+
if (mixedNative) {
|
|
3894
|
+
for (const nativeModelId of mixedNative.nativeModelIds) addModel(nativeModelId, "openai");
|
|
3895
|
+
}
|
|
3344
3896
|
sendJson(res, 200, {
|
|
3345
3897
|
object: "list",
|
|
3346
3898
|
data
|
|
3347
3899
|
});
|
|
3348
3900
|
return;
|
|
3349
3901
|
}
|
|
3350
|
-
if (req.method === "GET" &&
|
|
3351
|
-
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
|
+
}
|
|
3352
3908
|
const route = findCodexProxyRoute(routes, id);
|
|
3353
3909
|
if (!route) {
|
|
3354
3910
|
sendJson(res, 404, { error: { message: `Model not found: ${id}`, type: "invalid_request_error" } });
|
|
@@ -3362,8 +3918,8 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3362
3918
|
});
|
|
3363
3919
|
return;
|
|
3364
3920
|
}
|
|
3365
|
-
if (req.method === "POST" &&
|
|
3366
|
-
if (requireAuth) {
|
|
3921
|
+
if (req.method === "POST" && effectivePath === "/v1/responses") {
|
|
3922
|
+
if (requireAuth && !mixedPath) {
|
|
3367
3923
|
const inboundKey = extractApiKey(req);
|
|
3368
3924
|
if (!inboundKey || inboundKey !== PROXY_PLACEHOLDER_KEY) {
|
|
3369
3925
|
sendJson(res, 401, { error: { message: "Unauthorized", type: "invalid_api_key" } });
|
|
@@ -3414,7 +3970,49 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3414
3970
|
}
|
|
3415
3971
|
}
|
|
3416
3972
|
const modelId = String(body.model ?? "");
|
|
3417
|
-
|
|
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);
|
|
3418
4016
|
if (!resolved) {
|
|
3419
4017
|
const fallbackRoute = routes[0];
|
|
3420
4018
|
const fallbackLm = fallbackRoute ? models.get(fallbackRoute.modelId) : void 0;
|
|
@@ -3433,8 +4031,13 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3433
4031
|
}
|
|
3434
4032
|
const { route, languageModel } = resolved;
|
|
3435
4033
|
try {
|
|
4034
|
+
const routedBody = await prepareExternalCodexBody(body, {
|
|
4035
|
+
relay: nativePayloadRelay,
|
|
4036
|
+
mixedNative,
|
|
4037
|
+
headers: req.headers
|
|
4038
|
+
});
|
|
3436
4039
|
let params = applyClaudeCodeOAuthIdentity(route, translateResponsesRequest(
|
|
3437
|
-
|
|
4040
|
+
routedBody,
|
|
3438
4041
|
route.npm,
|
|
3439
4042
|
{
|
|
3440
4043
|
providerId: route.providerId,
|
|
@@ -3549,7 +4152,7 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3549
4152
|
sendJson(res, 404, { error: { message: "Not found", type: "invalid_request_error" } });
|
|
3550
4153
|
});
|
|
3551
4154
|
function wsAcceptKey(clientKey) {
|
|
3552
|
-
return
|
|
4155
|
+
return createHash2("sha1").update(clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64");
|
|
3553
4156
|
}
|
|
3554
4157
|
function wsDecodeFrame(buf) {
|
|
3555
4158
|
if (buf.length < 2) return null;
|
|
@@ -3564,9 +4167,12 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3564
4167
|
offset = 4;
|
|
3565
4168
|
} else if (payloadLen === 127) {
|
|
3566
4169
|
if (buf.length < 10) return null;
|
|
3567
|
-
|
|
4170
|
+
const declaredLength = buf.readBigUInt64BE(2);
|
|
4171
|
+
if (declaredLength > BigInt(MAX_CODEX_REQUEST_BYTES)) return { text: "", complete: true, opcode: -1 };
|
|
4172
|
+
payloadLen = Number(declaredLength);
|
|
3568
4173
|
offset = 10;
|
|
3569
4174
|
}
|
|
4175
|
+
if (payloadLen > MAX_CODEX_REQUEST_BYTES) return { text: "", complete: true, opcode: -1 };
|
|
3570
4176
|
const maskLen = masked ? 4 : 0;
|
|
3571
4177
|
if (buf.length < offset + maskLen + payloadLen) return null;
|
|
3572
4178
|
const mask = masked ? buf.slice(offset, offset + 4) : null;
|
|
@@ -3576,11 +4182,11 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3576
4182
|
payload[i] = buf[offset + i] ^ (mask ? mask[i % 4] : 0);
|
|
3577
4183
|
}
|
|
3578
4184
|
const opcode = b0 & 15;
|
|
3579
|
-
if (
|
|
3580
|
-
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 };
|
|
3581
4187
|
}
|
|
3582
|
-
function wsEncodeTextFrame(
|
|
3583
|
-
const payload = Buffer.from(
|
|
4188
|
+
function wsEncodeTextFrame(text5) {
|
|
4189
|
+
const payload = Buffer.from(text5, "utf8");
|
|
3584
4190
|
const len = payload.length;
|
|
3585
4191
|
let header;
|
|
3586
4192
|
if (len < 126) {
|
|
@@ -3598,13 +4204,33 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
3598
4204
|
}
|
|
3599
4205
|
return Buffer.concat([header, payload]);
|
|
3600
4206
|
}
|
|
3601
|
-
function wsCloseFrame() {
|
|
3602
|
-
|
|
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]);
|
|
3603
4211
|
}
|
|
3604
4212
|
function wsPingFrame() {
|
|
3605
4213
|
return Buffer.from([137, 0]);
|
|
3606
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
|
+
}
|
|
3607
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
|
+
}
|
|
3608
4234
|
if (requireAuth) {
|
|
3609
4235
|
const inboundKey = extractApiKey(req);
|
|
3610
4236
|
if (!inboundKey || inboundKey !== PROXY_PLACEHOLDER_KEY) {
|
|
@@ -3629,10 +4255,12 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
|
|
|
3629
4255
|
);
|
|
3630
4256
|
let frameBuf = Buffer.alloc(0);
|
|
3631
4257
|
let handled = false;
|
|
4258
|
+
let nativeActive = false;
|
|
4259
|
+
let nativeUpstream;
|
|
3632
4260
|
let currentRequestModel = "";
|
|
3633
|
-
const closeSocket = () => {
|
|
4261
|
+
const closeSocket = (code = 1e3) => {
|
|
3634
4262
|
if (!socket.destroyed) {
|
|
3635
|
-
socket.write(wsCloseFrame());
|
|
4263
|
+
socket.write(wsCloseFrame(code));
|
|
3636
4264
|
socket.end();
|
|
3637
4265
|
}
|
|
3638
4266
|
};
|
|
@@ -3658,10 +4286,28 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
|
|
|
3658
4286
|
};
|
|
3659
4287
|
const onData = (chunk) => {
|
|
3660
4288
|
frameBuf = Buffer.concat([frameBuf, chunk]);
|
|
3661
|
-
if (handled) return;
|
|
4289
|
+
if (handled && !nativeActive) return;
|
|
3662
4290
|
const frame = wsDecodeFrame(frameBuf);
|
|
3663
4291
|
if (!frame) return;
|
|
3664
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
|
+
}
|
|
3665
4311
|
handled = true;
|
|
3666
4312
|
void (async () => {
|
|
3667
4313
|
let body;
|
|
@@ -3682,6 +4328,9 @@ data: ${JSON.stringify({ error: { message: "Invalid JSON", type: "invalid_reques
|
|
|
3682
4328
|
const tools = Array.isArray(body.tools) ? body.tools : [];
|
|
3683
4329
|
const toolNames = tools.map((t) => t && typeof t === "object" && "name" in t ? t.name : "?").join(",");
|
|
3684
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"}]`);
|
|
3685
4334
|
appendCodexBodyDump({
|
|
3686
4335
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3687
4336
|
transport: "ws",
|
|
@@ -3694,7 +4343,145 @@ data: ${JSON.stringify({ error: { message: "Invalid JSON", type: "invalid_reques
|
|
|
3694
4343
|
}
|
|
3695
4344
|
const modelId = String(body.model ?? "");
|
|
3696
4345
|
currentRequestModel = modelId;
|
|
3697
|
-
|
|
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);
|
|
3698
4485
|
if (!resolved) {
|
|
3699
4486
|
const fb = routes[0];
|
|
3700
4487
|
const fbLm = fb ? models.get(fb.modelId) : void 0;
|
|
@@ -3713,8 +4500,13 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
|
|
|
3713
4500
|
}
|
|
3714
4501
|
const { route, languageModel } = resolved;
|
|
3715
4502
|
try {
|
|
4503
|
+
const routedBody = await prepareExternalCodexBody(body, {
|
|
4504
|
+
relay: nativePayloadRelay,
|
|
4505
|
+
mixedNative,
|
|
4506
|
+
headers: req.headers
|
|
4507
|
+
});
|
|
3716
4508
|
let params = applyClaudeCodeOAuthIdentity(route, translateResponsesRequest(
|
|
3717
|
-
|
|
4509
|
+
routedBody,
|
|
3718
4510
|
route.npm,
|
|
3719
4511
|
{
|
|
3720
4512
|
providerId: route.providerId,
|
|
@@ -3798,81 +4590,10 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
|
|
|
3798
4590
|
// src/codex/profile.ts
|
|
3799
4591
|
import { join as join3 } from "path";
|
|
3800
4592
|
|
|
3801
|
-
// src/codex/routing.ts
|
|
3802
|
-
function codexCompatibleProviders(providers, agent = "codex") {
|
|
3803
|
-
return providersForTarget(providers, agent);
|
|
3804
|
-
}
|
|
3805
|
-
function resolveBaseURL(model, provider) {
|
|
3806
|
-
if (provider.id === "zen" || provider.id === "go") {
|
|
3807
|
-
const isAnthropic = model.modelFormat === "anthropic";
|
|
3808
|
-
const baseUrl = BACKENDS[provider.id].baseUrl;
|
|
3809
|
-
return isAnthropic ? baseUrl : `${baseUrl}/v1`;
|
|
3810
|
-
}
|
|
3811
|
-
return model.apiBaseUrl ?? model.completionsUrl?.replace(/\/chat\/completions$/, "") ?? model.baseUrl;
|
|
3812
|
-
}
|
|
3813
|
-
function resolveCodexRoute(provider, model, apiKey) {
|
|
3814
|
-
const upstreamModelId = model.upstreamModelId || model.id;
|
|
3815
|
-
const inferredNpm = model.modelFormat === "anthropic" ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible";
|
|
3816
|
-
const isZenGo = provider.id === "zen" || provider.id === "go";
|
|
3817
|
-
const base = {
|
|
3818
|
-
npm: isZenGo ? inferredNpm : model.npm ?? inferredNpm,
|
|
3819
|
-
baseURL: resolveBaseURL(model, provider),
|
|
3820
|
-
upstreamModelId,
|
|
3821
|
-
apiKey,
|
|
3822
|
-
contextWindow: model.contextWindow,
|
|
3823
|
-
modelId: model.id,
|
|
3824
|
-
providerId: provider.id,
|
|
3825
|
-
authType: provider.authType,
|
|
3826
|
-
oauthAccountId: provider.oauthAccountId,
|
|
3827
|
-
providerData: provider.providerData,
|
|
3828
|
-
supportedParameters: model.supportedParameters,
|
|
3829
|
-
reasoning: model.reasoning,
|
|
3830
|
-
interleavedReasoningField: model.interleavedReasoningField,
|
|
3831
|
-
headers: provider.headers,
|
|
3832
|
-
refreshToken: providerRefreshToken(provider.id, provider.authType, provider.authRef)
|
|
3833
|
-
};
|
|
3834
|
-
if (model.modelFormat === "cloud-code") {
|
|
3835
|
-
return {
|
|
3836
|
-
tier: "cloud-code",
|
|
3837
|
-
npm: "@ai-sdk/anthropic",
|
|
3838
|
-
baseURL: "",
|
|
3839
|
-
upstreamModelId: model.upstreamModelId || model.id,
|
|
3840
|
-
apiKey,
|
|
3841
|
-
contextWindow: model.contextWindow,
|
|
3842
|
-
modelId: model.id,
|
|
3843
|
-
providerId: provider.id,
|
|
3844
|
-
authType: provider.authType,
|
|
3845
|
-
oauthAccountId: provider.oauthAccountId,
|
|
3846
|
-
providerData: provider.providerData,
|
|
3847
|
-
supportedParameters: model.supportedParameters,
|
|
3848
|
-
reasoning: model.reasoning,
|
|
3849
|
-
interleavedReasoningField: model.interleavedReasoningField,
|
|
3850
|
-
headers: provider.headers,
|
|
3851
|
-
refreshToken: providerRefreshToken(provider.id, provider.authType, provider.authRef)
|
|
3852
|
-
};
|
|
3853
|
-
}
|
|
3854
|
-
if (model.npm === "@ai-sdk/openai" && provider.authType !== "oauth" && model.modelFormat === "openai") {
|
|
3855
|
-
return { tier: "direct", ...base };
|
|
3856
|
-
}
|
|
3857
|
-
return { tier: "proxy", ...base };
|
|
3858
|
-
}
|
|
3859
|
-
function routableModelsForProvider(provider, agent = "codex") {
|
|
3860
|
-
return routableModelsForTarget(provider, agent);
|
|
3861
|
-
}
|
|
3862
|
-
function codexProviderEnvKey(providerId) {
|
|
3863
|
-
const known = {
|
|
3864
|
-
openai: "OPENAI_API_KEY",
|
|
3865
|
-
xai: "XAI_API_KEY",
|
|
3866
|
-
"xai-oauth": "XAI_API_KEY",
|
|
3867
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
3868
|
-
google: "GEMINI_API_KEY"
|
|
3869
|
-
};
|
|
3870
|
-
return known[providerId] ?? `${providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
3871
|
-
}
|
|
3872
|
-
|
|
3873
4593
|
// src/codex/session.ts
|
|
3874
4594
|
import {
|
|
3875
4595
|
copyFileSync,
|
|
4596
|
+
chmodSync,
|
|
3876
4597
|
existsSync as existsSync3,
|
|
3877
4598
|
mkdirSync,
|
|
3878
4599
|
readdirSync,
|
|
@@ -3888,8 +4609,8 @@ import { basename, dirname, join as join2 } from "path";
|
|
|
3888
4609
|
var CODEX_PROFILE_NAME = "relay-ai-launch";
|
|
3889
4610
|
var STALE_SESSION_MS = 5 * 60 * 1e3;
|
|
3890
4611
|
var MAX_BACKUPS = 5;
|
|
3891
|
-
function getCodexHome() {
|
|
3892
|
-
return join2(homedir3(), ".codex");
|
|
4612
|
+
function getCodexHome(env = process.env) {
|
|
4613
|
+
return env["CODEX_HOME"] || join2(homedir3(), ".codex");
|
|
3893
4614
|
}
|
|
3894
4615
|
function getCodexProfilePath() {
|
|
3895
4616
|
return join2(getCodexHome(), `${CODEX_PROFILE_NAME}.config.toml`);
|
|
@@ -3907,7 +4628,7 @@ function getCatalogPath(providerId, env = process.env) {
|
|
|
3907
4628
|
return join2(getRelayAiCodexDir(env), `models-${providerId}.json`);
|
|
3908
4629
|
}
|
|
3909
4630
|
function ownedOverlayPaths(env = process.env) {
|
|
3910
|
-
const paths = [getCodexProfilePath()
|
|
4631
|
+
const paths = [getCodexProfilePath()];
|
|
3911
4632
|
const codexDir = getRelayAiCodexDir(env);
|
|
3912
4633
|
if (existsSync3(codexDir)) {
|
|
3913
4634
|
for (const name of readdirSync(codexDir)) {
|
|
@@ -3916,13 +4637,26 @@ function ownedOverlayPaths(env = process.env) {
|
|
|
3916
4637
|
}
|
|
3917
4638
|
}
|
|
3918
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));
|
|
3919
4649
|
return paths;
|
|
3920
4650
|
}
|
|
3921
4651
|
function atomicWriteFile(path3, content) {
|
|
3922
4652
|
mkdirSync(dirname(path3), { recursive: true });
|
|
3923
4653
|
const tmp = `${path3}.tmp.${process.pid}`;
|
|
3924
|
-
writeFileSync(tmp, content, "utf8");
|
|
4654
|
+
writeFileSync(tmp, content, { encoding: "utf8", mode: 384 });
|
|
3925
4655
|
renameSync(tmp, path3);
|
|
4656
|
+
try {
|
|
4657
|
+
chmodSync(path3, 384);
|
|
4658
|
+
} catch {
|
|
4659
|
+
}
|
|
3926
4660
|
}
|
|
3927
4661
|
function rotateBackups(filePath, env = process.env) {
|
|
3928
4662
|
if (!existsSync3(filePath)) return;
|
|
@@ -4055,6 +4789,23 @@ env_key = "RELAY_AI_CODEX_KEY"
|
|
|
4055
4789
|
wire_api = "responses"
|
|
4056
4790
|
`;
|
|
4057
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
|
+
}
|
|
4058
4809
|
function getProfileOutputPath() {
|
|
4059
4810
|
return getCodexProfilePath();
|
|
4060
4811
|
}
|
|
@@ -4159,9 +4910,12 @@ function ensureCodexSandboxArgs(extraArgs) {
|
|
|
4159
4910
|
if (codexArgsIncludeSandboxFlag(extraArgs)) return extraArgs;
|
|
4160
4911
|
return ["-s", CODEX_LAUNCH_SANDBOX, ...extraArgs];
|
|
4161
4912
|
}
|
|
4162
|
-
function buildCodexChildEnv(route, proxyPort) {
|
|
4913
|
+
function buildCodexChildEnv(route, proxyPort, options = {}) {
|
|
4163
4914
|
const env = stripCodexInheritedEnv(process.env);
|
|
4164
|
-
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) {
|
|
4165
4919
|
env["RELAY_AI_CODEX_KEY"] = PROXY_PLACEHOLDER_KEY;
|
|
4166
4920
|
} else {
|
|
4167
4921
|
const envKey = codexProviderEnvKey(route.providerId);
|
|
@@ -4190,6 +4944,32 @@ function launchCodex(modelId, env, extraArgs) {
|
|
|
4190
4944
|
// src/codex/prompts.ts
|
|
4191
4945
|
import pc5 from "picocolors";
|
|
4192
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
|
+
}
|
|
4193
4973
|
async function pickCodexProvider(providers, prefs, hasFavorites = false, initialProviderId) {
|
|
4194
4974
|
if (providers.length === 0 && !hasFavorites) return null;
|
|
4195
4975
|
const options = providers.map((lp) => providerSelectOption(lp));
|
|
@@ -4542,6 +5322,136 @@ async function resolveCodexFavorites(activeProvider, selectedModel, compatible,
|
|
|
4542
5322
|
providersById: new Map(compatible.map((lp) => [lp.id, lp]))
|
|
4543
5323
|
};
|
|
4544
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
|
+
}
|
|
4545
5455
|
|
|
4546
5456
|
// src/cloud-code-backend.ts
|
|
4547
5457
|
function needsCloudCodeBackend(model, authType) {
|
|
@@ -4606,6 +5516,93 @@ async function startCloudCodeCatalogBackend(routes, startingAliasId, trace) {
|
|
|
4606
5516
|
return { port: handle.port, token: handle.token, handle };
|
|
4607
5517
|
}
|
|
4608
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
|
+
|
|
4609
5606
|
// src/agent-io.ts
|
|
4610
5607
|
var agentStdoutMode = false;
|
|
4611
5608
|
function setAgentStdoutMode(enabled) {
|
|
@@ -4791,6 +5788,8 @@ ${pc7.bold("Options:")}
|
|
|
4791
5788
|
--provider Boot provider id (skip wizard when paired with --model or non-interactive)
|
|
4792
5789
|
--model Boot model id (skip wizard when paired with --provider or non-interactive)
|
|
4793
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
|
|
4794
5793
|
--restore Remove interrupted-session overlay files
|
|
4795
5794
|
--config Preview/write launch configuration without starting Codex
|
|
4796
5795
|
--help Show this command help
|
|
@@ -4874,6 +5873,21 @@ async function writeFavoritesLaunchArtifacts(resolved, starting, proxyPort) {
|
|
|
4874
5873
|
}));
|
|
4875
5874
|
return { profilePath, catalogPath };
|
|
4876
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
|
+
}
|
|
4877
5891
|
function printCodexCleanupReminder(hadProxy) {
|
|
4878
5892
|
if (isAgentStdoutMode()) return;
|
|
4879
5893
|
const left = remainingOverlayPaths();
|
|
@@ -5098,7 +6112,13 @@ Error: ${launchPlan.error}
|
|
|
5098
6112
|
return 0;
|
|
5099
6113
|
}
|
|
5100
6114
|
const favorites = prefs.favoriteModels ?? [];
|
|
5101
|
-
|
|
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;
|
|
5102
6122
|
if (favoritesActive && !configOnly) {
|
|
5103
6123
|
p8.log.info(
|
|
5104
6124
|
`Favorites mode active \u2014 Codex picker will show ${favorites.length + 1} models (1 starting + ${favorites.length} favorites).`
|
|
@@ -5171,6 +6191,42 @@ Error: ${launchPlan.error}
|
|
|
5171
6191
|
return 1;
|
|
5172
6192
|
}
|
|
5173
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
|
+
}
|
|
5174
6230
|
if (!configOnly && !(launchPlan.skip && launchPlan.target)) {
|
|
5175
6231
|
const modelLabel = formatCodexModelLabel(selectedModel);
|
|
5176
6232
|
const confirmed = await confirmCodexLaunch(
|
|
@@ -5182,11 +6238,21 @@ Error: ${launchPlan.error}
|
|
|
5182
6238
|
if (!confirmed) return 0;
|
|
5183
6239
|
}
|
|
5184
6240
|
let proxyHandle = null;
|
|
5185
|
-
let cloudCodeBackend = null;
|
|
5186
|
-
let cloudCodeBackendFav = null;
|
|
5187
6241
|
try {
|
|
5188
6242
|
let proxyPort;
|
|
5189
|
-
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) {
|
|
5190
6256
|
const needsBackend = (r) => {
|
|
5191
6257
|
const m = r.model;
|
|
5192
6258
|
const prov = providersById.get(r.providerId);
|
|
@@ -5302,7 +6368,7 @@ Error: ${launchPlan.error}
|
|
|
5302
6368
|
const startingFavorite = resolvedFavorites.find(
|
|
5303
6369
|
(r) => r.providerId === activeProvider.id && r.model.id === selectedModel.id
|
|
5304
6370
|
) ?? resolvedFavorites[0];
|
|
5305
|
-
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);
|
|
5306
6372
|
writeSessionLock({
|
|
5307
6373
|
pid: process.pid,
|
|
5308
6374
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -5316,7 +6382,11 @@ Error: ${launchPlan.error}
|
|
|
5316
6382
|
console.log("");
|
|
5317
6383
|
console.log(pc7.bold(pc7.cyan(" CONFIG PREVIEW \u2014 relay-ai codex")));
|
|
5318
6384
|
console.log("");
|
|
5319
|
-
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) {
|
|
5320
6390
|
console.log(` ${pc7.bold("Mode:")} Favorites Catalog (${resolvedFavorites.length} model${resolvedFavorites.length !== 1 ? "s" : ""})`);
|
|
5321
6391
|
console.log("");
|
|
5322
6392
|
console.log(` ${pc7.bold("Models:")}`);
|
|
@@ -5347,7 +6417,7 @@ Error: ${launchPlan.error}
|
|
|
5347
6417
|
}
|
|
5348
6418
|
}
|
|
5349
6419
|
const favoritesLaunch = favoritesActive && resolvedFavorites.length > 0;
|
|
5350
|
-
const launchModelId = favoritesLaunch ? codexCliFavoritesSlug(activeProvider.id, selectedModel.id) : selectedModel.id;
|
|
6420
|
+
const launchModelId = favoritesLaunch ? codexCliFavoritesSlug(activeProvider.id, selectedModel.id) : mixedPlan?.selectedSlug ?? selectedModel.id;
|
|
5351
6421
|
if (!agentStdout) {
|
|
5352
6422
|
logActiveModel(modelLabel, launchModelId);
|
|
5353
6423
|
printCodexCliCleanupPanel("relay-ai codex --restore");
|
|
@@ -5363,9 +6433,10 @@ Error: ${launchPlan.error}
|
|
|
5363
6433
|
};
|
|
5364
6434
|
const childEnv = buildCodexChildEnv(
|
|
5365
6435
|
favoritesLaunch || route.tier === "cloud-code" ? dummyRoute : route,
|
|
5366
|
-
proxyPort
|
|
6436
|
+
proxyPort,
|
|
6437
|
+
{ mixedNative: !!mixedPlan }
|
|
5367
6438
|
);
|
|
5368
|
-
const hadProxy = (route.tier === "proxy" || route.tier === "cloud-code" || favoritesLaunch) && !!proxyPort;
|
|
6439
|
+
const hadProxy = (!!mixedPlan || route.tier === "proxy" || route.tier === "cloud-code" || favoritesLaunch) && !!proxyPort;
|
|
5369
6440
|
const exitCode = await launchCodex(launchModelId, childEnv, passthroughArgs);
|
|
5370
6441
|
if (trace) printTraceLog(debugLogPath);
|
|
5371
6442
|
printCodexCleanupReminder(hadProxy);
|
|
@@ -5390,15 +6461,15 @@ import * as p10 from "@clack/prompts";
|
|
|
5390
6461
|
import { spawn as spawn3 } from "child_process";
|
|
5391
6462
|
import { existsSync as existsSync5, mkdirSync as mkdirSync2, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5392
6463
|
import { homedir as homedir5, tmpdir } from "os";
|
|
5393
|
-
import { join as
|
|
6464
|
+
import { join as join6 } from "path";
|
|
5394
6465
|
var isWindows3 = process.platform === "win32";
|
|
5395
6466
|
var GEMINI_API_KEY_AUTH_TYPE = "gemini-api-key";
|
|
5396
6467
|
var GEMINI_FALLBACK_PATHS = isWindows3 ? [
|
|
5397
|
-
|
|
5398
|
-
|
|
6468
|
+
join6(process.env["APPDATA"] ?? homedir5(), "npm", "gemini.cmd"),
|
|
6469
|
+
join6(process.env["APPDATA"] ?? homedir5(), "npm", "gemini")
|
|
5399
6470
|
] : [
|
|
5400
|
-
|
|
5401
|
-
|
|
6471
|
+
join6(homedir5(), ".local", "bin", "gemini"),
|
|
6472
|
+
join6(homedir5(), ".npm", "bin", "gemini"),
|
|
5402
6473
|
"/usr/local/bin/gemini",
|
|
5403
6474
|
"/opt/homebrew/bin/gemini"
|
|
5404
6475
|
];
|
|
@@ -5419,7 +6490,7 @@ function buildGeminiChildEnv(proxyPort, proxyToken) {
|
|
|
5419
6490
|
return env;
|
|
5420
6491
|
}
|
|
5421
6492
|
function createGeminiCliHomeOverlay() {
|
|
5422
|
-
const cliHome = mkdtempSync(
|
|
6493
|
+
const cliHome = mkdtempSync(join6(tmpdir(), "relay-ai-gemini-"));
|
|
5423
6494
|
const settings = {
|
|
5424
6495
|
security: {
|
|
5425
6496
|
auth: {
|
|
@@ -5427,9 +6498,9 @@ function createGeminiCliHomeOverlay() {
|
|
|
5427
6498
|
}
|
|
5428
6499
|
}
|
|
5429
6500
|
};
|
|
5430
|
-
const geminiDir =
|
|
6501
|
+
const geminiDir = join6(cliHome, ".gemini");
|
|
5431
6502
|
mkdirSync2(geminiDir);
|
|
5432
|
-
writeFileSync2(
|
|
6503
|
+
writeFileSync2(join6(geminiDir, "settings.json"), `${JSON.stringify(settings, null, 2)}
|
|
5433
6504
|
`, {
|
|
5434
6505
|
encoding: "utf8",
|
|
5435
6506
|
mode: 384
|
|
@@ -5646,8 +6717,8 @@ function mergeConsecutiveMessages2(messages) {
|
|
|
5646
6717
|
}
|
|
5647
6718
|
return merged;
|
|
5648
6719
|
}
|
|
5649
|
-
function stripGeminiIdentity(
|
|
5650
|
-
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");
|
|
5651
6722
|
}
|
|
5652
6723
|
function translateGeminiRequest(body, options = {}) {
|
|
5653
6724
|
let system;
|
|
@@ -5665,16 +6736,16 @@ function translateGeminiRequest(body, options = {}) {
|
|
|
5665
6736
|
const turnParts = turn.parts || [];
|
|
5666
6737
|
for (const p15 of turnParts) {
|
|
5667
6738
|
if (p15.text !== void 0) {
|
|
5668
|
-
const
|
|
5669
|
-
if (
|
|
5670
|
-
const tokens =
|
|
6739
|
+
const text5 = stripGeminiIdentity(p15.text);
|
|
6740
|
+
if (text5.includes("<thinking>")) {
|
|
6741
|
+
const tokens = text5.split(/<thinking>([\s\S]*?)<\/thinking>/);
|
|
5671
6742
|
for (let i = 0; i < tokens.length; i++) {
|
|
5672
6743
|
const token = tokens[i].trim();
|
|
5673
6744
|
if (!token) continue;
|
|
5674
6745
|
parts.push({ type: i % 2 === 1 ? "reasoning" : "text", text: token });
|
|
5675
6746
|
}
|
|
5676
6747
|
} else {
|
|
5677
|
-
parts.push({ type: "text", text:
|
|
6748
|
+
parts.push({ type: "text", text: text5 });
|
|
5678
6749
|
}
|
|
5679
6750
|
} else if (p15.inlineData) {
|
|
5680
6751
|
parts.push({
|
|
@@ -5859,14 +6930,14 @@ ${rawBody}`);
|
|
|
5859
6930
|
const current = sessionRouteOverride ?? (lookupGeminiRoute(routes, requestedModel) ?? defaultRoute);
|
|
5860
6931
|
const availableList = routes.map((r) => ` - ${r.aliasId} (${r.displayName})`).join("\n");
|
|
5861
6932
|
const exampleId = routes.length > 1 ? routes[1].aliasId : routes[0]?.aliasId ?? "deepseek-v4";
|
|
5862
|
-
const
|
|
6933
|
+
const text5 = `Current model: ${current.displayName} (${current.aliasId})
|
|
5863
6934
|
|
|
5864
6935
|
Available models:
|
|
5865
6936
|
${availableList}
|
|
5866
6937
|
|
|
5867
6938
|
\u{1F4A1} To switch models, type: .model <id>
|
|
5868
6939
|
Example: .model ${exampleId}`;
|
|
5869
|
-
sendMockGeminiResponse(res,
|
|
6940
|
+
sendMockGeminiResponse(res, text5, isStream, current.aliasId);
|
|
5870
6941
|
return;
|
|
5871
6942
|
}
|
|
5872
6943
|
const targetRoute = lookupGeminiRoute(routes, modelCommand);
|
|
@@ -5929,33 +7000,33 @@ ${JSON.stringify(params, null, 2)}`);
|
|
|
5929
7000
|
`);
|
|
5930
7001
|
}
|
|
5931
7002
|
if (p15.type === "reasoning") {
|
|
5932
|
-
let
|
|
7003
|
+
let text5 = p15.textDelta ?? p15.text ?? "";
|
|
5933
7004
|
if (!isThinking) {
|
|
5934
7005
|
isThinking = true;
|
|
5935
|
-
|
|
5936
|
-
` +
|
|
7006
|
+
text5 = `<thinking>
|
|
7007
|
+
` + text5;
|
|
5937
7008
|
}
|
|
5938
7009
|
const chunk = {
|
|
5939
|
-
candidates: [{ content: { role: "model", parts: [{ text:
|
|
7010
|
+
candidates: [{ content: { role: "model", parts: [{ text: text5 }] } }],
|
|
5940
7011
|
modelVersion: route.aliasId
|
|
5941
7012
|
};
|
|
5942
7013
|
res.write(`data: ${JSON.stringify(chunk)}
|
|
5943
7014
|
|
|
5944
7015
|
`);
|
|
5945
7016
|
} else if (p15.type === "text-delta") {
|
|
5946
|
-
let
|
|
7017
|
+
let text5 = p15.textDelta ?? p15.text ?? "";
|
|
5947
7018
|
if (isThinking) {
|
|
5948
7019
|
isThinking = false;
|
|
5949
|
-
|
|
7020
|
+
text5 = `
|
|
5950
7021
|
</thinking>
|
|
5951
7022
|
|
|
5952
|
-
` +
|
|
7023
|
+
` + text5;
|
|
5953
7024
|
}
|
|
5954
7025
|
const chunk = {
|
|
5955
7026
|
candidates: [{
|
|
5956
7027
|
content: {
|
|
5957
7028
|
role: "model",
|
|
5958
|
-
parts: [{ text:
|
|
7029
|
+
parts: [{ text: text5 }]
|
|
5959
7030
|
}
|
|
5960
7031
|
}],
|
|
5961
7032
|
modelVersion: route.aliasId
|
|
@@ -6138,28 +7209,28 @@ function parseModelCommand(turn) {
|
|
|
6138
7209
|
if (!turn || turn.role !== "user") return null;
|
|
6139
7210
|
const parts = turn.parts || [];
|
|
6140
7211
|
if (parts.length !== 1) return null;
|
|
6141
|
-
const
|
|
6142
|
-
if (typeof
|
|
6143
|
-
const trimmed =
|
|
7212
|
+
const text5 = parts[0]?.text;
|
|
7213
|
+
if (typeof text5 !== "string") return null;
|
|
7214
|
+
const trimmed = text5.trim();
|
|
6144
7215
|
if (!trimmed.startsWith(".model")) return null;
|
|
6145
7216
|
if (trimmed === ".model") return "";
|
|
6146
7217
|
if (trimmed.charAt(6) !== " ") return null;
|
|
6147
7218
|
return trimmed.slice(7).trim();
|
|
6148
7219
|
}
|
|
6149
|
-
function sendMockGeminiResponse(res,
|
|
7220
|
+
function sendMockGeminiResponse(res, text5, isStream, modelVersion) {
|
|
6150
7221
|
if (isStream) {
|
|
6151
7222
|
res.writeHead(200, {
|
|
6152
7223
|
"Content-Type": "text/event-stream",
|
|
6153
7224
|
"Cache-Control": "no-cache",
|
|
6154
7225
|
"Connection": "keep-alive"
|
|
6155
7226
|
});
|
|
6156
|
-
writeGeminiStreamText(res,
|
|
7227
|
+
writeGeminiStreamText(res, text5, modelVersion);
|
|
6157
7228
|
res.end();
|
|
6158
7229
|
} else {
|
|
6159
7230
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6160
7231
|
res.end(JSON.stringify({
|
|
6161
7232
|
candidates: [{
|
|
6162
|
-
content: { role: "model", parts: [{ text:
|
|
7233
|
+
content: { role: "model", parts: [{ text: text5 }] },
|
|
6163
7234
|
finishReason: "STOP"
|
|
6164
7235
|
}],
|
|
6165
7236
|
usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 },
|
|
@@ -6167,10 +7238,10 @@ function sendMockGeminiResponse(res, text4, isStream, modelVersion) {
|
|
|
6167
7238
|
}));
|
|
6168
7239
|
}
|
|
6169
7240
|
}
|
|
6170
|
-
function writeGeminiStreamText(res,
|
|
7241
|
+
function writeGeminiStreamText(res, text5, modelVersion) {
|
|
6171
7242
|
const chunk = {
|
|
6172
7243
|
candidates: [{
|
|
6173
|
-
content: { role: "model", parts: [{ text:
|
|
7244
|
+
content: { role: "model", parts: [{ text: text5 }] },
|
|
6174
7245
|
finishReason: "STOP"
|
|
6175
7246
|
}],
|
|
6176
7247
|
usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 },
|
|
@@ -7987,11 +9058,11 @@ async function handleCloudCodeForwardRequest(res, route, parsed, lowerUrl, log14
|
|
|
7987
9058
|
});
|
|
7988
9059
|
res.end(body);
|
|
7989
9060
|
}
|
|
7990
|
-
function emitThinkingDelta(res, route, responseId,
|
|
7991
|
-
if (!
|
|
9061
|
+
function emitThinkingDelta(res, route, responseId, text5, startSse) {
|
|
9062
|
+
if (!text5) return;
|
|
7992
9063
|
startSse();
|
|
7993
9064
|
const chunk = formatCloudCodeChunk({
|
|
7994
|
-
thought:
|
|
9065
|
+
thought: text5,
|
|
7995
9066
|
modelVersion: route.catalogId,
|
|
7996
9067
|
responseId
|
|
7997
9068
|
});
|
|
@@ -7999,9 +9070,9 @@ function emitThinkingDelta(res, route, responseId, text4, startSse) {
|
|
|
7999
9070
|
|
|
8000
9071
|
`);
|
|
8001
9072
|
}
|
|
8002
|
-
function trailingPartial(
|
|
8003
|
-
for (let len = Math.min(tag.length - 1,
|
|
8004
|
-
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;
|
|
8005
9076
|
}
|
|
8006
9077
|
return 0;
|
|
8007
9078
|
}
|
|
@@ -8013,13 +9084,13 @@ function createThinkFilter() {
|
|
|
8013
9084
|
let src = partial + chunk;
|
|
8014
9085
|
partial = "";
|
|
8015
9086
|
let thought = "";
|
|
8016
|
-
let
|
|
9087
|
+
let text5 = "";
|
|
8017
9088
|
while (src.length > 0) {
|
|
8018
9089
|
if (state === "scanning") {
|
|
8019
9090
|
const idx = src.indexOf("<think>");
|
|
8020
9091
|
if (idx === -1) {
|
|
8021
9092
|
const len = trailingPartial(src, "<think>");
|
|
8022
|
-
|
|
9093
|
+
text5 += src.slice(0, src.length - len);
|
|
8023
9094
|
if (len > 0) {
|
|
8024
9095
|
partial = src.slice(src.length - len);
|
|
8025
9096
|
} else {
|
|
@@ -8027,7 +9098,7 @@ function createThinkFilter() {
|
|
|
8027
9098
|
}
|
|
8028
9099
|
break;
|
|
8029
9100
|
}
|
|
8030
|
-
|
|
9101
|
+
text5 += src.slice(0, idx);
|
|
8031
9102
|
src = src.slice(idx + 7);
|
|
8032
9103
|
state = "inside";
|
|
8033
9104
|
} else {
|
|
@@ -8044,7 +9115,7 @@ function createThinkFilter() {
|
|
|
8044
9115
|
state = "passthrough";
|
|
8045
9116
|
}
|
|
8046
9117
|
}
|
|
8047
|
-
return { thought, text:
|
|
9118
|
+
return { thought, text: text5 };
|
|
8048
9119
|
};
|
|
8049
9120
|
}
|
|
8050
9121
|
function emitStreamError(res, route, responseId, message, startSse) {
|
|
@@ -8106,8 +9177,8 @@ function respondUnsupportedMedia(res, route, streaming) {
|
|
|
8106
9177
|
metadata: {}
|
|
8107
9178
|
});
|
|
8108
9179
|
}
|
|
8109
|
-
function parsePseudoToolCall(
|
|
8110
|
-
const trimmed =
|
|
9180
|
+
function parsePseudoToolCall(text5, knownToolNames) {
|
|
9181
|
+
const trimmed = text5.trim();
|
|
8111
9182
|
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
8112
9183
|
try {
|
|
8113
9184
|
const obj = JSON.parse(trimmed);
|
|
@@ -8196,18 +9267,18 @@ async function handleStreamingRequest(res, route, providerOptions, parsed, log14
|
|
|
8196
9267
|
continue;
|
|
8197
9268
|
}
|
|
8198
9269
|
if (p15.type === "text-delta") {
|
|
8199
|
-
const { thought, text:
|
|
9270
|
+
const { thought, text: text5 } = thinkFilter(reasoningDeltaText(p15));
|
|
8200
9271
|
if (thought) {
|
|
8201
9272
|
responseReasoning += thought;
|
|
8202
9273
|
emitThinkingDelta(res, route, responseId, thought, startSse);
|
|
8203
9274
|
}
|
|
8204
|
-
if (
|
|
8205
|
-
log14(`[gateway] text-delta: ${JSON.stringify(
|
|
8206
|
-
if (!bufferingJsonText && (textBuffer +
|
|
9275
|
+
if (text5) {
|
|
9276
|
+
log14(`[gateway] text-delta: ${JSON.stringify(text5.slice(0, 500))}`);
|
|
9277
|
+
if (!bufferingJsonText && (textBuffer + text5).trimStart().startsWith("{")) {
|
|
8207
9278
|
bufferingJsonText = true;
|
|
8208
9279
|
}
|
|
8209
9280
|
if (bufferingJsonText) {
|
|
8210
|
-
textBuffer +=
|
|
9281
|
+
textBuffer += text5;
|
|
8211
9282
|
const pseudoTool = textBuffer.trimEnd().endsWith("}") ? parsePseudoToolCall(textBuffer, knownToolNames) : null;
|
|
8212
9283
|
if (pseudoTool) {
|
|
8213
9284
|
log14(`[gateway] parsed pseudo tool-call from text: ${pseudoTool.name}`);
|
|
@@ -8216,7 +9287,7 @@ async function handleStreamingRequest(res, route, providerOptions, parsed, log14
|
|
|
8216
9287
|
} else {
|
|
8217
9288
|
startSse();
|
|
8218
9289
|
const chunk = formatCloudCodeChunk({
|
|
8219
|
-
text:
|
|
9290
|
+
text: text5,
|
|
8220
9291
|
modelVersion: route.catalogId,
|
|
8221
9292
|
responseId
|
|
8222
9293
|
});
|
|
@@ -8406,19 +9477,19 @@ async function resolveAntigravityLaunchRoutes(opts) {
|
|
|
8406
9477
|
}
|
|
8407
9478
|
|
|
8408
9479
|
// src/antigravity/launch-cli.ts
|
|
8409
|
-
import { execFileSync as
|
|
9480
|
+
import { execFileSync as execFileSync3, execSync as execSync3 } from "child_process";
|
|
8410
9481
|
import spawn4 from "cross-spawn";
|
|
8411
9482
|
import { existsSync as existsSync6 } from "fs";
|
|
8412
9483
|
import { homedir as homedir6 } from "os";
|
|
8413
|
-
import { join as
|
|
9484
|
+
import { join as join7 } from "path";
|
|
8414
9485
|
var isWindows4 = process.platform === "win32";
|
|
8415
9486
|
var FALLBACK_PATHS = isWindows4 ? [
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
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")
|
|
8419
9490
|
] : [
|
|
8420
|
-
|
|
8421
|
-
|
|
9491
|
+
join7(homedir6(), ".local", "bin", "agy"),
|
|
9492
|
+
join7(homedir6(), ".npm", "bin", "agy"),
|
|
8422
9493
|
"/usr/local/bin/agy",
|
|
8423
9494
|
"/opt/homebrew/bin/agy"
|
|
8424
9495
|
];
|
|
@@ -8444,7 +9515,7 @@ function readAntigravityCliVersion(binaryPath = findAntigravityCliBinary() ?? vo
|
|
|
8444
9515
|
return { version: null, error: 'Antigravity CLI binary "agy" not found' };
|
|
8445
9516
|
}
|
|
8446
9517
|
try {
|
|
8447
|
-
const raw =
|
|
9518
|
+
const raw = execFileSync3(binaryPath, ["--version"], {
|
|
8448
9519
|
encoding: "utf8",
|
|
8449
9520
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8450
9521
|
}).trim();
|
|
@@ -8493,10 +9564,10 @@ function launchAntigravityCli(env, extraArgs) {
|
|
|
8493
9564
|
}
|
|
8494
9565
|
|
|
8495
9566
|
// src/antigravity/launch-ide.ts
|
|
8496
|
-
import { execFileSync as
|
|
9567
|
+
import { execFileSync as execFileSync4, execSync as execSync4, spawn as spawn5 } from "child_process";
|
|
8497
9568
|
import { existsSync as existsSync7 } from "fs";
|
|
8498
9569
|
import { homedir as homedir7 } from "os";
|
|
8499
|
-
import { join as
|
|
9570
|
+
import { join as join8 } from "path";
|
|
8500
9571
|
|
|
8501
9572
|
// src/antigravity/ide-profile.ts
|
|
8502
9573
|
import fs from "fs";
|
|
@@ -8530,8 +9601,8 @@ function prepareIdeProfile(profileDir, gatewayUrl) {
|
|
|
8530
9601
|
}
|
|
8531
9602
|
|
|
8532
9603
|
// src/antigravity/launch-ide.ts
|
|
8533
|
-
var LINUX_APP_PROFILE_DIR =
|
|
8534
|
-
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");
|
|
8535
9606
|
function sleep(ms) {
|
|
8536
9607
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
8537
9608
|
}
|
|
@@ -8539,7 +9610,7 @@ function linuxAntigravityBinary() {
|
|
|
8539
9610
|
const candidates = [
|
|
8540
9611
|
"/usr/share/antigravity/antigravity",
|
|
8541
9612
|
"/opt/antigravity/antigravity",
|
|
8542
|
-
|
|
9613
|
+
join8(homedir7(), ".local", "share", "antigravity", "antigravity")
|
|
8543
9614
|
];
|
|
8544
9615
|
for (const candidate of candidates) {
|
|
8545
9616
|
if (existsSync7(candidate)) return candidate;
|
|
@@ -8597,7 +9668,7 @@ function defaultProcessList() {
|
|
|
8597
9668
|
const psArgs = process.platform === "linux" ? ["-eo", "pid=,args="] : ["-axo", "pid=,command="];
|
|
8598
9669
|
if (process.platform !== "darwin" && process.platform !== "linux") return "";
|
|
8599
9670
|
try {
|
|
8600
|
-
return
|
|
9671
|
+
return execFileSync4("ps", psArgs, {
|
|
8601
9672
|
encoding: "utf8",
|
|
8602
9673
|
stdio: ["ignore", "pipe", "ignore"],
|
|
8603
9674
|
maxBuffer: 1024 * 1024 * 4
|
|
@@ -8661,11 +9732,11 @@ function quitAntigravityIdeGracefully() {
|
|
|
8661
9732
|
}
|
|
8662
9733
|
if (process.platform !== "darwin") return;
|
|
8663
9734
|
try {
|
|
8664
|
-
|
|
9735
|
+
execFileSync4("osascript", ["-e", 'tell application "Antigravity IDE" to quit'], {
|
|
8665
9736
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8666
9737
|
});
|
|
8667
9738
|
} catch {
|
|
8668
|
-
|
|
9739
|
+
execFileSync4("osascript", ["-e", 'tell application id "com.google.antigravity-ide" to quit'], {
|
|
8669
9740
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8670
9741
|
});
|
|
8671
9742
|
}
|
|
@@ -8681,11 +9752,11 @@ function quitAntigravityAppGracefully() {
|
|
|
8681
9752
|
}
|
|
8682
9753
|
if (process.platform !== "darwin") return;
|
|
8683
9754
|
try {
|
|
8684
|
-
|
|
9755
|
+
execFileSync4("osascript", ["-e", 'tell application "Antigravity" to quit'], {
|
|
8685
9756
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8686
9757
|
});
|
|
8687
9758
|
} catch {
|
|
8688
|
-
|
|
9759
|
+
execFileSync4("osascript", ["-e", 'tell application id "com.google.antigravity" to quit'], {
|
|
8689
9760
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8690
9761
|
});
|
|
8691
9762
|
}
|
|
@@ -8694,15 +9765,15 @@ function findAntigravityAppBinary() {
|
|
|
8694
9765
|
const override = getAppPathOverride("antigravity");
|
|
8695
9766
|
if (override) return existsSync7(override) ? override : null;
|
|
8696
9767
|
if (process.platform === "win32") {
|
|
8697
|
-
const localAppData = process.env["LOCALAPPDATA"] ??
|
|
8698
|
-
const winPath =
|
|
9768
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? join8(homedir7(), "AppData", "Local");
|
|
9769
|
+
const winPath = join8(localAppData, "Programs", "Antigravity", "Antigravity.exe");
|
|
8699
9770
|
return existsSync7(winPath) ? winPath : null;
|
|
8700
9771
|
}
|
|
8701
9772
|
if (process.platform === "linux") return linuxAntigravityBinary();
|
|
8702
9773
|
if (process.platform !== "darwin") return null;
|
|
8703
9774
|
const defaultPath = "/Applications/Antigravity.app/Contents/MacOS/Antigravity";
|
|
8704
9775
|
if (existsSync7(defaultPath)) return defaultPath;
|
|
8705
|
-
const homePath =
|
|
9776
|
+
const homePath = join8(homedir7(), "Applications", "Antigravity.app", "Contents", "MacOS", "Antigravity");
|
|
8706
9777
|
if (existsSync7(homePath)) return homePath;
|
|
8707
9778
|
return null;
|
|
8708
9779
|
}
|
|
@@ -8710,15 +9781,15 @@ function findAntigravityIdeBinary() {
|
|
|
8710
9781
|
const override = getAppPathOverride("antigravity-ide");
|
|
8711
9782
|
if (override) return existsSync7(override) ? override : null;
|
|
8712
9783
|
if (process.platform === "win32") {
|
|
8713
|
-
const localAppData = process.env["LOCALAPPDATA"] ??
|
|
8714
|
-
const winPath =
|
|
9784
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? join8(homedir7(), "AppData", "Local");
|
|
9785
|
+
const winPath = join8(localAppData, "Programs", "Antigravity IDE", "Antigravity IDE.exe");
|
|
8715
9786
|
return existsSync7(winPath) ? winPath : null;
|
|
8716
9787
|
}
|
|
8717
9788
|
if (process.platform === "linux") return linuxAntigravityBinary();
|
|
8718
9789
|
if (process.platform !== "darwin") return null;
|
|
8719
9790
|
const defaultPath = "/Applications/Antigravity IDE.app/Contents/Resources/app/bin/antigravity-ide";
|
|
8720
9791
|
if (existsSync7(defaultPath)) return defaultPath;
|
|
8721
|
-
const homePath =
|
|
9792
|
+
const homePath = join8(homedir7(), "Applications", "Antigravity IDE.app", "Contents", "Resources", "app", "bin", "antigravity-ide");
|
|
8722
9793
|
if (existsSync7(homePath)) return homePath;
|
|
8723
9794
|
return null;
|
|
8724
9795
|
}
|
|
@@ -8779,7 +9850,7 @@ function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
|
|
|
8779
9850
|
return;
|
|
8780
9851
|
}
|
|
8781
9852
|
prepareIdeProfile(profileDir, gatewayUrl);
|
|
8782
|
-
const relayExtensionsDir =
|
|
9853
|
+
const relayExtensionsDir = join8(homedir7(), ".relay-ai", "antigravity", "extensions");
|
|
8783
9854
|
const args = [
|
|
8784
9855
|
`--user-data-dir=${profileDir}`,
|
|
8785
9856
|
`--extensions-dir=${relayExtensionsDir}`,
|
|
@@ -8809,7 +9880,7 @@ function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
|
|
|
8809
9880
|
|
|
8810
9881
|
// src/antigravity.ts
|
|
8811
9882
|
import { homedir as homedir8 } from "os";
|
|
8812
|
-
import { join as
|
|
9883
|
+
import { join as join9 } from "path";
|
|
8813
9884
|
var SHUTDOWN_DRAIN_MS = 500;
|
|
8814
9885
|
var AGY_FAVORITES_PROVIDER_ID = "__relay_agy_favorites__";
|
|
8815
9886
|
var AGY_FAVORITES_PROVIDER_LABEL = "\u2605 Antigravity CLI Favorites";
|
|
@@ -9103,7 +10174,7 @@ async function runAntigravityAppCommand(childArgs, trace = false, boot) {
|
|
|
9103
10174
|
trace,
|
|
9104
10175
|
boot,
|
|
9105
10176
|
async (env, _routes, gatewayHandle) => {
|
|
9106
|
-
const profileDir =
|
|
10177
|
+
const profileDir = join9(homedir8(), ".relay-ai", "antigravity", "app-profile");
|
|
9107
10178
|
if (isAntigravityAppRunning(profileDir)) {
|
|
9108
10179
|
const restart = await p11.confirm({
|
|
9109
10180
|
message: "Restart Antigravity to apply this Relay gateway?",
|
|
@@ -9151,7 +10222,7 @@ async function runAntigravityIdeCommand(childArgs, trace = false, boot) {
|
|
|
9151
10222
|
trace,
|
|
9152
10223
|
boot,
|
|
9153
10224
|
async (env, _routes, gatewayHandle) => {
|
|
9154
|
-
const profileDir =
|
|
10225
|
+
const profileDir = join9(homedir8(), ".relay-ai", "antigravity", "profile");
|
|
9155
10226
|
if (isAntigravityIdeRunning(profileDir)) {
|
|
9156
10227
|
const restart = await p11.confirm({
|
|
9157
10228
|
message: "Restart Antigravity IDE to apply this Relay gateway?",
|
|
@@ -9196,6 +10267,8 @@ async function runAntigravityIdeCommand(childArgs, trace = false, boot) {
|
|
|
9196
10267
|
// src/codex-app.ts
|
|
9197
10268
|
import pc10 from "picocolors";
|
|
9198
10269
|
import * as p12 from "@clack/prompts";
|
|
10270
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
10271
|
+
import { join as join12 } from "path";
|
|
9199
10272
|
|
|
9200
10273
|
// src/codex/app-provider-routes.ts
|
|
9201
10274
|
function codexRouteToProxyRoute(provider, model, apiKey) {
|
|
@@ -9285,13 +10358,13 @@ async function buildCodexAppProviderCatalogRoutes(provider, apiKey, selectedMode
|
|
|
9285
10358
|
|
|
9286
10359
|
// src/codex/app-config.ts
|
|
9287
10360
|
import { existsSync as existsSync8, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
9288
|
-
import { dirname as dirname2, join as
|
|
10361
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
9289
10362
|
import { parse, stringify } from "smol-toml";
|
|
9290
10363
|
function getCodexConfigPath() {
|
|
9291
|
-
return
|
|
10364
|
+
return join10(getCodexHome(), "config.toml");
|
|
9292
10365
|
}
|
|
9293
10366
|
function getCodexAppSidecarProfilePath() {
|
|
9294
|
-
return
|
|
10367
|
+
return join10(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
|
|
9295
10368
|
}
|
|
9296
10369
|
function asRecord(value) {
|
|
9297
10370
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -9313,16 +10386,20 @@ function applyRestoreNumber(config, key, had, value) {
|
|
|
9313
10386
|
delete config[key];
|
|
9314
10387
|
}
|
|
9315
10388
|
}
|
|
10389
|
+
function isMultiAgentV2Enabled(value) {
|
|
10390
|
+
if (value === true) return true;
|
|
10391
|
+
return asRecord(value).enabled === true;
|
|
10392
|
+
}
|
|
9316
10393
|
function readCodexConfigText(path3 = getCodexConfigPath()) {
|
|
9317
10394
|
if (!existsSync8(path3)) return "";
|
|
9318
10395
|
return readFileSync3(path3, "utf8");
|
|
9319
10396
|
}
|
|
9320
|
-
function parseCodexConfig(
|
|
9321
|
-
if (!
|
|
9322
|
-
return asRecord(parse(
|
|
10397
|
+
function parseCodexConfig(text5) {
|
|
10398
|
+
if (!text5.trim()) return {};
|
|
10399
|
+
return asRecord(parse(text5));
|
|
9323
10400
|
}
|
|
9324
|
-
function captureRestoreState(
|
|
9325
|
-
const config = parseCodexConfig(
|
|
10401
|
+
function captureRestoreState(text5) {
|
|
10402
|
+
const config = parseCodexConfig(text5);
|
|
9326
10403
|
const profile = rootString(config, "profile");
|
|
9327
10404
|
const model = rootString(config, "model");
|
|
9328
10405
|
const modelProvider = rootString(config, "model_provider");
|
|
@@ -9331,6 +10408,8 @@ function captureRestoreState(text4) {
|
|
|
9331
10408
|
const reasoning = rootString(config, "model_reasoning_effort");
|
|
9332
10409
|
const contextWindow = rootNumber(config, "model_context_window");
|
|
9333
10410
|
const autoCompact = rootNumber(config, "model_auto_compact_token_limit");
|
|
10411
|
+
const features = asRecord(config.features);
|
|
10412
|
+
const multiAgentV2 = "multi_agent_v2" in features;
|
|
9334
10413
|
return {
|
|
9335
10414
|
hadProfile: profile.had,
|
|
9336
10415
|
profile: profile.value,
|
|
@@ -9347,16 +10426,18 @@ function captureRestoreState(text4) {
|
|
|
9347
10426
|
hadModelContextWindow: contextWindow.had,
|
|
9348
10427
|
modelContextWindow: contextWindow.value,
|
|
9349
10428
|
hadModelAutoCompactTokenLimit: autoCompact.had,
|
|
9350
|
-
modelAutoCompactTokenLimit: autoCompact.value
|
|
10429
|
+
modelAutoCompactTokenLimit: autoCompact.value,
|
|
10430
|
+
hadMultiAgentV2: multiAgentV2,
|
|
10431
|
+
multiAgentV2: features.multi_agent_v2
|
|
9351
10432
|
};
|
|
9352
10433
|
}
|
|
9353
|
-
function isAppManagedConfig(
|
|
9354
|
-
const config = parseCodexConfig(
|
|
10434
|
+
function isAppManagedConfig(text5) {
|
|
10435
|
+
const config = parseCodexConfig(text5);
|
|
9355
10436
|
const mp = rootString(config, "model_provider");
|
|
9356
10437
|
if (mp.had && mp.value === CODEX_APP_PROVIDER_ID) return true;
|
|
9357
10438
|
const baseUrl = rootString(config, "openai_base_url");
|
|
9358
10439
|
const catalog = rootString(config, "model_catalog_json");
|
|
9359
|
-
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);
|
|
9360
10441
|
}
|
|
9361
10442
|
function mergeAppConfig(existing, spec) {
|
|
9362
10443
|
const patch = buildCodexAppRootConfig(spec);
|
|
@@ -9376,6 +10457,16 @@ function mergeAppConfig(existing, spec) {
|
|
|
9376
10457
|
} else {
|
|
9377
10458
|
delete out.model_auto_compact_token_limit;
|
|
9378
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;
|
|
9379
10470
|
const providers = asRecord(out.model_providers);
|
|
9380
10471
|
delete providers[CODEX_APP_PROVIDER_ID];
|
|
9381
10472
|
const profiles = asRecord(out.profiles);
|
|
@@ -9409,8 +10500,8 @@ function mergeAppConfig(existing, spec) {
|
|
|
9409
10500
|
}
|
|
9410
10501
|
return out;
|
|
9411
10502
|
}
|
|
9412
|
-
function validateAppConfigText(
|
|
9413
|
-
const config = parseCodexConfig(
|
|
10503
|
+
function validateAppConfigText(text5, spec) {
|
|
10504
|
+
const config = parseCodexConfig(text5);
|
|
9414
10505
|
if ("profile" in config) {
|
|
9415
10506
|
throw new Error("Generated config still contains legacy root profile key");
|
|
9416
10507
|
}
|
|
@@ -9423,13 +10514,17 @@ function validateAppConfigText(text4, spec) {
|
|
|
9423
10514
|
throw new Error("Generated config must keep the built-in OpenAI model_provider");
|
|
9424
10515
|
}
|
|
9425
10516
|
const baseUrl = rootString(config, "openai_base_url");
|
|
9426
|
-
|
|
10517
|
+
const expectedBaseUrl = spec.proxyBaseUrl ?? `http://127.0.0.1:${spec.proxyPort}/v1`;
|
|
10518
|
+
if (baseUrl.value !== expectedBaseUrl) {
|
|
9427
10519
|
throw new Error("Generated config openai_base_url mismatch");
|
|
9428
10520
|
}
|
|
9429
10521
|
const catalog = rootString(config, "model_catalog_json");
|
|
9430
10522
|
if (catalog.value !== spec.catalogPath) {
|
|
9431
10523
|
throw new Error("Generated config model_catalog_json mismatch");
|
|
9432
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
|
+
}
|
|
9433
10528
|
}
|
|
9434
10529
|
function applyAppConfigPatch(spec, configPath = getCodexConfigPath()) {
|
|
9435
10530
|
const existingText = readCodexConfigText(configPath);
|
|
@@ -9440,12 +10535,12 @@ function applyAppConfigPatch(spec, configPath = getCodexConfigPath()) {
|
|
|
9440
10535
|
throw new Error(`Invalid existing Codex config at ${configPath}: ${err instanceof Error ? err.message : err}`);
|
|
9441
10536
|
}
|
|
9442
10537
|
const merged = mergeAppConfig(existing, spec);
|
|
9443
|
-
const
|
|
10538
|
+
const text5 = `${stringify(merged)}
|
|
9444
10539
|
`;
|
|
9445
|
-
validateAppConfigText(
|
|
10540
|
+
validateAppConfigText(text5, spec);
|
|
9446
10541
|
mkdirSync3(dirname2(configPath), { recursive: true });
|
|
9447
|
-
writeFileSync3(configPath,
|
|
9448
|
-
return
|
|
10542
|
+
writeFileSync3(configPath, text5, "utf8");
|
|
10543
|
+
return text5;
|
|
9449
10544
|
}
|
|
9450
10545
|
function applyRestoreKey(config, key, had, value) {
|
|
9451
10546
|
if (had && value !== void 0) {
|
|
@@ -9464,6 +10559,14 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
|
|
|
9464
10559
|
} else {
|
|
9465
10560
|
config.model_providers = providers;
|
|
9466
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;
|
|
9467
10570
|
if (state.hadProfile && state.profile) {
|
|
9468
10571
|
config.profile = state.profile;
|
|
9469
10572
|
} else {
|
|
@@ -9497,10 +10600,10 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
|
|
|
9497
10600
|
return true;
|
|
9498
10601
|
}
|
|
9499
10602
|
function previewAppConfigToml(spec) {
|
|
9500
|
-
const
|
|
10603
|
+
const text5 = `${stringify(buildCodexAppRootConfig(spec))}
|
|
9501
10604
|
`;
|
|
9502
|
-
validateAppConfigText(
|
|
9503
|
-
return
|
|
10605
|
+
validateAppConfigText(text5, spec);
|
|
10606
|
+
return text5;
|
|
9504
10607
|
}
|
|
9505
10608
|
|
|
9506
10609
|
// src/codex/app-session.ts
|
|
@@ -9510,17 +10613,18 @@ import {
|
|
|
9510
10613
|
mkdirSync as mkdirSync4,
|
|
9511
10614
|
readdirSync as readdirSync2,
|
|
9512
10615
|
readFileSync as readFileSync4,
|
|
9513
|
-
rmSync as rmSync4
|
|
10616
|
+
rmSync as rmSync4,
|
|
10617
|
+
statSync as statSync2
|
|
9514
10618
|
} from "fs";
|
|
9515
|
-
import { basename as basename2, join as
|
|
10619
|
+
import { basename as basename2, join as join11 } from "path";
|
|
9516
10620
|
function getAppSessionLockPath(env = process.env) {
|
|
9517
|
-
return
|
|
10621
|
+
return join11(getRelayAiCodexDir(env), "session-app.json");
|
|
9518
10622
|
}
|
|
9519
10623
|
function getAppRestoreStatePath(env = process.env) {
|
|
9520
|
-
return
|
|
10624
|
+
return join11(getRelayAiCodexDir(env), "app-restore-state.json");
|
|
9521
10625
|
}
|
|
9522
10626
|
function getAppCatalogPath(providerId, env = process.env) {
|
|
9523
|
-
return
|
|
10627
|
+
return join11(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
|
|
9524
10628
|
}
|
|
9525
10629
|
function readAppSessionLock(env = process.env) {
|
|
9526
10630
|
const path3 = getAppSessionLockPath(env);
|
|
@@ -9565,24 +10669,24 @@ function backupConfigToml(env = process.env) {
|
|
|
9565
10669
|
const backupsDir = getBackupsDir(env);
|
|
9566
10670
|
mkdirSync4(backupsDir, { recursive: true });
|
|
9567
10671
|
const base = basename2(configPath);
|
|
9568
|
-
const backupPath =
|
|
10672
|
+
const backupPath = join11(backupsDir, `${base}.${Date.now()}.bak`);
|
|
9569
10673
|
copyFileSync2(configPath, backupPath);
|
|
9570
10674
|
return backupPath;
|
|
9571
10675
|
}
|
|
9572
10676
|
function saveAppRestoreStateBeforePatch(env = process.env) {
|
|
9573
|
-
const
|
|
10677
|
+
const text5 = readCodexConfigText();
|
|
9574
10678
|
const existing = readAppRestoreState(env);
|
|
9575
|
-
if (existing && isAppManagedConfig(
|
|
10679
|
+
if (existing && isAppManagedConfig(text5)) {
|
|
9576
10680
|
return existing;
|
|
9577
10681
|
}
|
|
9578
|
-
const state = captureRestoreState(
|
|
10682
|
+
const state = captureRestoreState(text5);
|
|
9579
10683
|
writeAppRestoreState(state, env);
|
|
9580
10684
|
return state;
|
|
9581
10685
|
}
|
|
9582
10686
|
function ownedAppCatalogPaths(env = process.env) {
|
|
9583
10687
|
const codexDir = getRelayAiCodexDir(env);
|
|
9584
10688
|
if (!existsSync9(codexDir)) return [];
|
|
9585
|
-
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));
|
|
9586
10690
|
}
|
|
9587
10691
|
function removeAppCatalogs(env = process.env) {
|
|
9588
10692
|
const removed = [];
|
|
@@ -9595,6 +10699,20 @@ function removeAppCatalogs(env = process.env) {
|
|
|
9595
10699
|
}
|
|
9596
10700
|
return removed;
|
|
9597
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
|
+
}
|
|
9598
10716
|
function restoreCodexAppOverlay(env = process.env) {
|
|
9599
10717
|
const lock = readAppSessionLock(env);
|
|
9600
10718
|
if (lock && lock.pid !== process.pid && isConcurrentSession(lock)) {
|
|
@@ -9604,8 +10722,8 @@ function restoreCodexAppOverlay(env = process.env) {
|
|
|
9604
10722
|
message: `Another relay-ai codex-app session is running (pid ${lock.pid}). Ctrl+C it first, then run --restore.`
|
|
9605
10723
|
};
|
|
9606
10724
|
}
|
|
9607
|
-
const
|
|
9608
|
-
const managed = isAppManagedConfig(
|
|
10725
|
+
const text5 = readCodexConfigText();
|
|
10726
|
+
const managed = isAppManagedConfig(text5);
|
|
9609
10727
|
const restoreState = readAppRestoreState(env);
|
|
9610
10728
|
if (!managed && !restoreState && !lock) {
|
|
9611
10729
|
removeAppCatalogs(env);
|
|
@@ -9616,6 +10734,9 @@ function restoreCodexAppOverlay(env = process.env) {
|
|
|
9616
10734
|
restoreConfigFromState(restoreState);
|
|
9617
10735
|
} else if (lock?.backupPath && existsSync9(lock.backupPath)) {
|
|
9618
10736
|
copyFileSync2(lock.backupPath, getCodexConfigPath());
|
|
10737
|
+
} else if (managed) {
|
|
10738
|
+
const backupPath = newestConfigBackup(env);
|
|
10739
|
+
if (backupPath) copyFileSync2(backupPath, getCodexConfigPath());
|
|
9619
10740
|
}
|
|
9620
10741
|
removeAppCatalogs(env);
|
|
9621
10742
|
clearAppRestoreState(env);
|
|
@@ -9723,6 +10844,8 @@ ${pc10.bold("Usage:")}
|
|
|
9723
10844
|
|
|
9724
10845
|
${pc10.bold("Options:")}
|
|
9725
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
|
|
9726
10849
|
--restore Restore Codex config after an interrupted app session
|
|
9727
10850
|
--config Preview the generated Codex app configuration without launching
|
|
9728
10851
|
--trace Write proxy debug logs to ~/.relay-ai/logs/ and show errors on exit
|
|
@@ -9735,7 +10858,7 @@ ${pc10.bold("Description:")}
|
|
|
9735
10858
|
ChatGPT desktop app in Codex mode. Keep this terminal open while using Codex.
|
|
9736
10859
|
|
|
9737
10860
|
${pc10.bold("Platforms:")}
|
|
9738
|
-
macOS and
|
|
10861
|
+
macOS, Windows, and Linux (ChatGPT desktop app preview).
|
|
9739
10862
|
|
|
9740
10863
|
${pc10.bold("Cleanup:")}
|
|
9741
10864
|
Ctrl+C stops the proxy and restores your previous Codex config.
|
|
@@ -9964,7 +11087,13 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
9964
11087
|
}
|
|
9965
11088
|
const prefs = loadPreferences();
|
|
9966
11089
|
const favorites = prefs.favoriteModels ?? [];
|
|
9967
|
-
|
|
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;
|
|
9968
11097
|
if (favoritesActive && !configOnly) {
|
|
9969
11098
|
p12.log.info(
|
|
9970
11099
|
`Favorites mode active \u2014 Codex App picker will show ${favorites.length + 1} models (1 starting + ${favorites.length} favorites).`
|
|
@@ -10028,7 +11157,7 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
10028
11157
|
activeProvider.apiKey = apiKey;
|
|
10029
11158
|
let cloudCodeBackend = null;
|
|
10030
11159
|
let cloudCodeBackendFav = null;
|
|
10031
|
-
const appProviderRoutes = favoritesActive ? null : await buildCodexAppProviderCatalogRoutes(activeProvider, apiKey, selectedModel.id, trace);
|
|
11160
|
+
const appProviderRoutes = mixedMode || favoritesActive ? null : await buildCodexAppProviderCatalogRoutes(activeProvider, apiKey, selectedModel.id, trace);
|
|
10032
11161
|
cloudCodeBackend = appProviderRoutes?.backend ?? null;
|
|
10033
11162
|
const route = appProviderRoutes ? codexProxyRouteToCodexRoute(appProviderRoutes.selectedRoute, activeProvider.id) : resolveCodexRoute(activeProvider, selectedModel, apiKey);
|
|
10034
11163
|
const appRoute = { ...route, tier: "proxy" };
|
|
@@ -10041,6 +11170,42 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
10041
11170
|
resolvedFavorites = res.resolvedFavorites;
|
|
10042
11171
|
providersById = res.providersById;
|
|
10043
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
|
+
}
|
|
10044
11209
|
if (!configOnly) {
|
|
10045
11210
|
const modelLabel = formatCodexModelLabel(selectedModel);
|
|
10046
11211
|
const confirmed = await confirmCodexLaunch(
|
|
@@ -10057,8 +11222,16 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
10057
11222
|
let proxyHandle = null;
|
|
10058
11223
|
let sessionActive = false;
|
|
10059
11224
|
try {
|
|
10060
|
-
const catalogPath = favoritesActive && resolvedFavorites.length > 0 ? getFavoritesAppCatalogPath() : getAppCatalogPath(route.providerId);
|
|
10061
|
-
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 ? {
|
|
10062
11235
|
tier: "proxy",
|
|
10063
11236
|
modelId: codexCliFavoritesSlug(activeProvider.id, selectedModel.id),
|
|
10064
11237
|
providerId: activeProvider.id,
|
|
@@ -10074,7 +11247,11 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
10074
11247
|
console.log("");
|
|
10075
11248
|
console.log(pc10.bold(pc10.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app")));
|
|
10076
11249
|
console.log("");
|
|
10077
|
-
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) {
|
|
10078
11255
|
console.log(` ${pc10.bold("Mode:")} Favorites Catalog (${resolvedFavorites.length} model${resolvedFavorites.length !== 1 ? "s" : ""})`);
|
|
10079
11256
|
console.log("");
|
|
10080
11257
|
console.log(` ${pc10.bold("Models:")}`);
|
|
@@ -10091,7 +11268,9 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
10091
11268
|
console.log(` ${pc10.bold("config.toml patch preview:")}`);
|
|
10092
11269
|
const tomlPreview = previewAppConfigToml({
|
|
10093
11270
|
...specBase,
|
|
10094
|
-
proxyPort: PREVIEW_PROXY_PORT
|
|
11271
|
+
proxyPort: PREVIEW_PROXY_PORT,
|
|
11272
|
+
...mixedPlan?.multiAgentV2Enabled ? { multiAgentV2Enabled: true } : {},
|
|
11273
|
+
...mixedPlan ? { proxyBaseUrl: `${mixedProxyBaseUrl(PREVIEW_PROXY_PORT, mixedPlan.capability)}/v1` } : {}
|
|
10095
11274
|
});
|
|
10096
11275
|
for (const line of tomlPreview.split("\n")) {
|
|
10097
11276
|
console.log(` ${pc10.dim(line)}`);
|
|
@@ -10106,7 +11285,19 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
10106
11285
|
return 0;
|
|
10107
11286
|
}
|
|
10108
11287
|
let proxyPort;
|
|
10109
|
-
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) {
|
|
10110
11301
|
const needsBackend = (r) => {
|
|
10111
11302
|
const m = r.model;
|
|
10112
11303
|
const prov = providersById.get(r.providerId);
|
|
@@ -10155,12 +11346,14 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
10155
11346
|
proxyPort = proxyHandle.port;
|
|
10156
11347
|
}
|
|
10157
11348
|
const modelLabel = formatCodexModelLabel(selectedModel);
|
|
10158
|
-
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);
|
|
10159
11350
|
writeOverlayFile(catalogPath, serializeCatalog(catalogFile));
|
|
10160
11351
|
const spec = {
|
|
10161
11352
|
route: activeRoute,
|
|
10162
11353
|
proxyPort,
|
|
10163
|
-
catalogPath
|
|
11354
|
+
catalogPath,
|
|
11355
|
+
...mixedPlan?.multiAgentV2Enabled ? { multiAgentV2Enabled: true } : {},
|
|
11356
|
+
...mixedPlan ? { proxyBaseUrl: `${mixedProxyBaseUrl(proxyPort, mixedPlan.capability)}/v1` } : {}
|
|
10164
11357
|
};
|
|
10165
11358
|
saveAppRestoreStateBeforePatch();
|
|
10166
11359
|
const backupPath = backupConfigToml();
|
|
@@ -10225,22 +11418,22 @@ import * as p13 from "@clack/prompts";
|
|
|
10225
11418
|
// src/claude-desktop/app-config.ts
|
|
10226
11419
|
import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
|
|
10227
11420
|
import { homedir as homedir9 } from "os";
|
|
10228
|
-
import { join as
|
|
11421
|
+
import { join as join13, dirname as dirname3 } from "path";
|
|
10229
11422
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
10230
11423
|
function getClaudeDesktopHome() {
|
|
10231
11424
|
if (process.platform === "win32") {
|
|
10232
|
-
return
|
|
11425
|
+
return join13(process.env.LOCALAPPDATA || join13(homedir9(), "AppData", "Local"), "Claude-3p");
|
|
10233
11426
|
}
|
|
10234
11427
|
if (process.platform === "linux") {
|
|
10235
|
-
return
|
|
11428
|
+
return join13(process.env.XDG_CONFIG_HOME || join13(homedir9(), ".config"), "Claude-3p");
|
|
10236
11429
|
}
|
|
10237
|
-
return
|
|
11430
|
+
return join13(homedir9(), "Library", "Application Support", "Claude-3p");
|
|
10238
11431
|
}
|
|
10239
11432
|
function getConfigLibraryPath() {
|
|
10240
|
-
return
|
|
11433
|
+
return join13(getClaudeDesktopHome(), "configLibrary");
|
|
10241
11434
|
}
|
|
10242
11435
|
function getMetaJsonPath() {
|
|
10243
|
-
return
|
|
11436
|
+
return join13(getConfigLibraryPath(), "_meta.json");
|
|
10244
11437
|
}
|
|
10245
11438
|
function readMetaJson() {
|
|
10246
11439
|
const metaPath = getMetaJsonPath();
|
|
@@ -10268,7 +11461,7 @@ function buildRelayAiConfig(proxyPort) {
|
|
|
10268
11461
|
}
|
|
10269
11462
|
function writeRelayAiConfig(proxyPort) {
|
|
10270
11463
|
const uuid = randomUUID2();
|
|
10271
|
-
const configPath =
|
|
11464
|
+
const configPath = join13(getConfigLibraryPath(), `${uuid}.json`);
|
|
10272
11465
|
const config = buildRelayAiConfig(proxyPort);
|
|
10273
11466
|
mkdirSync5(dirname3(configPath), { recursive: true });
|
|
10274
11467
|
writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
|
|
@@ -10433,9 +11626,9 @@ import {
|
|
|
10433
11626
|
unlinkSync as unlinkSync2,
|
|
10434
11627
|
writeFileSync as writeFileSync5
|
|
10435
11628
|
} from "fs";
|
|
10436
|
-
import { dirname as dirname4, join as
|
|
11629
|
+
import { dirname as dirname4, join as join14 } from "path";
|
|
10437
11630
|
function getSessionLockPath2() {
|
|
10438
|
-
return
|
|
11631
|
+
return join14(getClaudeDesktopHome(), ".relay-ai.lock");
|
|
10439
11632
|
}
|
|
10440
11633
|
function inspectSessionLock() {
|
|
10441
11634
|
const path3 = getSessionLockPath2();
|
|
@@ -10489,7 +11682,7 @@ function restoreMetaJson() {
|
|
|
10489
11682
|
}
|
|
10490
11683
|
}
|
|
10491
11684
|
function removeRelayAiConfig(uuid) {
|
|
10492
|
-
const configPath =
|
|
11685
|
+
const configPath = join14(getConfigLibraryPath(), `${uuid}.json`);
|
|
10493
11686
|
if (existsSync11(configPath)) {
|
|
10494
11687
|
try {
|
|
10495
11688
|
rmSync5(configPath, { force: true });
|
|
@@ -10809,15 +12002,15 @@ ${pc11.bold("Claude Desktop 3P Mode Active")}`);
|
|
|
10809
12002
|
// src/ai-doc.ts
|
|
10810
12003
|
import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
10811
12004
|
import { homedir as homedir10 } from "os";
|
|
10812
|
-
import { join as
|
|
12005
|
+
import { join as join15 } from "path";
|
|
10813
12006
|
var SKILL_DIR_NAME = "relay-ai-cli";
|
|
10814
12007
|
var SKILL_INSTALL_DIRS = [
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
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")
|
|
10821
12014
|
];
|
|
10822
12015
|
function parseSkillVersion(content) {
|
|
10823
12016
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -10831,7 +12024,7 @@ function parseSkillVersion(content) {
|
|
|
10831
12024
|
return null;
|
|
10832
12025
|
}
|
|
10833
12026
|
function readInstalledSkillVersion(skillDir) {
|
|
10834
|
-
const skillPath =
|
|
12027
|
+
const skillPath = join15(skillDir, "SKILL.md");
|
|
10835
12028
|
if (!existsSync12(skillPath)) return null;
|
|
10836
12029
|
try {
|
|
10837
12030
|
const head = readFileSync7(skillPath, "utf-8").slice(0, 1024);
|
|
@@ -10844,8 +12037,8 @@ function readInstalledSkillVersion(skillDir) {
|
|
|
10844
12037
|
}
|
|
10845
12038
|
function skillInstallTargets() {
|
|
10846
12039
|
return SKILL_INSTALL_DIRS.map((dir) => {
|
|
10847
|
-
const skillDir =
|
|
10848
|
-
return { skillDir, skillPath:
|
|
12040
|
+
const skillDir = join15(dir, SKILL_DIR_NAME);
|
|
12041
|
+
return { skillDir, skillPath: join15(skillDir, "SKILL.md") };
|
|
10849
12042
|
});
|
|
10850
12043
|
}
|
|
10851
12044
|
function formatProviderModels(provider) {
|
|
@@ -11060,6 +12253,14 @@ FAVORITES / MID-SESSION SWITCHING:
|
|
|
11060
12253
|
Exception: Claude --http-proxy combines the selected compatible model with
|
|
11061
12254
|
compatible saved favorites while keeping native Anthropic models available.
|
|
11062
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
|
+
|
|
11063
12264
|
================================================================================
|
|
11064
12265
|
COMMANDS
|
|
11065
12266
|
================================================================================
|
|
@@ -11140,14 +12341,16 @@ PROVIDERS REGISTRY
|
|
|
11140
12341
|
MODELS / FAVORITES
|
|
11141
12342
|
relay-ai models manage favoriteModels in config (alias: favorites)
|
|
11142
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.
|
|
11143
12345
|
|
|
11144
12346
|
API GATEWAY (for tools that speak Anthropic/OpenAI HTTP)
|
|
11145
12347
|
relay-ai server foreground gateway on port 17645
|
|
11146
12348
|
relay-ai server --vertex Vertex AI gateway (gcloud ADC)
|
|
11147
12349
|
|
|
11148
12350
|
DESKTOP APPS
|
|
11149
|
-
relay-ai codex-app ChatGPT desktop, Codex mode (macOS/Windows); alias: chatgpt
|
|
11150
|
-
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.
|
|
11151
12354
|
|
|
11152
12355
|
================================================================================
|
|
11153
12356
|
CONFIGURATION PATHS
|
|
@@ -11498,24 +12701,24 @@ function buildHttpProxyChildEnv(baseEnv, proxyUrl, caCertPath) {
|
|
|
11498
12701
|
}
|
|
11499
12702
|
|
|
11500
12703
|
// src/http-proxy/ca.ts
|
|
11501
|
-
import { randomBytes, randomUUID as randomUUID3 } from "crypto";
|
|
12704
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "crypto";
|
|
11502
12705
|
import {
|
|
11503
|
-
chmodSync,
|
|
12706
|
+
chmodSync as chmodSync2,
|
|
11504
12707
|
existsSync as existsSync13,
|
|
11505
12708
|
mkdirSync as mkdirSync9,
|
|
11506
12709
|
readFileSync as readFileSync8,
|
|
11507
12710
|
readdirSync as readdirSync3,
|
|
11508
12711
|
rmSync as rmSync6,
|
|
11509
|
-
statSync as
|
|
12712
|
+
statSync as statSync3,
|
|
11510
12713
|
writeFileSync as writeFileSync8
|
|
11511
12714
|
} from "fs";
|
|
11512
|
-
import { dirname as dirname6, join as
|
|
12715
|
+
import { dirname as dirname6, join as join17, resolve } from "path";
|
|
11513
12716
|
import forge from "node-forge";
|
|
11514
12717
|
var SESSION_ROOT = "http-proxy-sessions";
|
|
11515
12718
|
var OWNER_FILE = "owner.pid";
|
|
11516
12719
|
var MID_CREATION_GRACE_MS = 3e4;
|
|
11517
12720
|
function serialNumber() {
|
|
11518
|
-
const bytes =
|
|
12721
|
+
const bytes = randomBytes2(16);
|
|
11519
12722
|
bytes[0] &= 127;
|
|
11520
12723
|
return bytes.toString("hex");
|
|
11521
12724
|
}
|
|
@@ -11529,15 +12732,15 @@ function processIsRunning(pid) {
|
|
|
11529
12732
|
}
|
|
11530
12733
|
}
|
|
11531
12734
|
function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
|
|
11532
|
-
const root =
|
|
12735
|
+
const root = join17(appHome, SESSION_ROOT);
|
|
11533
12736
|
if (!existsSync13(root)) return;
|
|
11534
12737
|
const now = Date.now();
|
|
11535
12738
|
for (const name of readdirSync3(root)) {
|
|
11536
|
-
const sessionDir =
|
|
12739
|
+
const sessionDir = join17(root, name);
|
|
11537
12740
|
try {
|
|
11538
|
-
const stat =
|
|
12741
|
+
const stat = statSync3(sessionDir);
|
|
11539
12742
|
if (!stat.isDirectory()) continue;
|
|
11540
|
-
const ownerPath =
|
|
12743
|
+
const ownerPath = join17(sessionDir, OWNER_FILE);
|
|
11541
12744
|
if (!existsSync13(ownerPath)) {
|
|
11542
12745
|
if (now - stat.mtimeMs > MID_CREATION_GRACE_MS) {
|
|
11543
12746
|
rmSync6(sessionDir, { recursive: true, force: true });
|
|
@@ -11546,7 +12749,7 @@ function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
|
|
|
11546
12749
|
}
|
|
11547
12750
|
const pid = Number(readFileSync8(ownerPath, "utf8").trim());
|
|
11548
12751
|
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
11549
|
-
const ownerStat =
|
|
12752
|
+
const ownerStat = statSync3(ownerPath);
|
|
11550
12753
|
const newestMtimeMs = Math.max(stat.mtimeMs, ownerStat.mtimeMs);
|
|
11551
12754
|
if (now - newestMtimeMs > MID_CREATION_GRACE_MS) {
|
|
11552
12755
|
rmSync6(sessionDir, { recursive: true, force: true });
|
|
@@ -11560,13 +12763,13 @@ function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
|
|
|
11560
12763
|
}
|
|
11561
12764
|
function createHttpProxyCertificates(appHome = getAppHome()) {
|
|
11562
12765
|
cleanupStaleHttpProxySessions(appHome);
|
|
11563
|
-
const root =
|
|
12766
|
+
const root = join17(appHome, SESSION_ROOT);
|
|
11564
12767
|
mkdirSync9(root, { recursive: true, mode: 448 });
|
|
11565
|
-
|
|
11566
|
-
const sessionDir =
|
|
12768
|
+
chmodSync2(root, 448);
|
|
12769
|
+
const sessionDir = join17(root, randomUUID3());
|
|
11567
12770
|
mkdirSync9(sessionDir, { mode: 448 });
|
|
11568
|
-
|
|
11569
|
-
writeFileSync8(
|
|
12771
|
+
chmodSync2(sessionDir, 448);
|
|
12772
|
+
writeFileSync8(join17(sessionDir, OWNER_FILE), `${process.pid}
|
|
11570
12773
|
`, { mode: 384 });
|
|
11571
12774
|
try {
|
|
11572
12775
|
const caKeys = forge.pki.rsa.generateKeyPair(2048);
|
|
@@ -11601,9 +12804,9 @@ function createHttpProxyCertificates(appHome = getAppHome()) {
|
|
|
11601
12804
|
]);
|
|
11602
12805
|
server.sign(caKeys.privateKey, forge.md.sha256.create());
|
|
11603
12806
|
const caCert = forge.pki.certificateToPem(ca);
|
|
11604
|
-
const caCertPath =
|
|
12807
|
+
const caCertPath = join17(sessionDir, "relay-ai-ca.pem");
|
|
11605
12808
|
writeFileSync8(caCertPath, caCert, { encoding: "utf8", mode: 384 });
|
|
11606
|
-
|
|
12809
|
+
chmodSync2(caCertPath, 384);
|
|
11607
12810
|
let cleaned = false;
|
|
11608
12811
|
const cleanupOnExit = () => {
|
|
11609
12812
|
if (cleaned) return;
|
|
@@ -11647,7 +12850,7 @@ function createHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
|
|
|
11647
12850
|
const relayCa = readFileSync8(relayCaCertPath, "utf8").trimEnd();
|
|
11648
12851
|
const additionalCa = readFileSync8(additionalCaCertPath, "utf8").trim();
|
|
11649
12852
|
if (!additionalCa) return relayCaCertPath;
|
|
11650
|
-
const combinedPath =
|
|
12853
|
+
const combinedPath = join17(dirname6(relayCaCertPath), "combined-ca.pem");
|
|
11651
12854
|
writeFileSync8(
|
|
11652
12855
|
combinedPath,
|
|
11653
12856
|
`${relayCa}
|
|
@@ -11655,7 +12858,7 @@ ${additionalCa}
|
|
|
11655
12858
|
`,
|
|
11656
12859
|
{ encoding: "utf8", mode: 384 }
|
|
11657
12860
|
);
|
|
11658
|
-
|
|
12861
|
+
chmodSync2(combinedPath, 384);
|
|
11659
12862
|
return combinedPath;
|
|
11660
12863
|
}
|
|
11661
12864
|
|
|
@@ -11663,7 +12866,7 @@ ${additionalCa}
|
|
|
11663
12866
|
import * as http2 from "http";
|
|
11664
12867
|
import * as https from "https";
|
|
11665
12868
|
import * as net from "net";
|
|
11666
|
-
import { randomBytes as
|
|
12869
|
+
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
11667
12870
|
import { URL as URL2 } from "url";
|
|
11668
12871
|
var ANTHROPIC_HOST = RELAY_SENTINEL_HOST;
|
|
11669
12872
|
var MAX_BODY_BYTES = 50 * 1024 * 1024;
|
|
@@ -12049,7 +13252,7 @@ async function startHttpProxy(options) {
|
|
|
12049
13252
|
});
|
|
12050
13253
|
mitmServer.on("tlsClientError", () => {
|
|
12051
13254
|
});
|
|
12052
|
-
const password3 =
|
|
13255
|
+
const password3 = randomBytes3(32).toString("base64url");
|
|
12053
13256
|
const expectedAuthorization = `Basic ${Buffer.from(`${PROXY_USERNAME}:${password3}`).toString("base64")}`;
|
|
12054
13257
|
const sockets = /* @__PURE__ */ new Set();
|
|
12055
13258
|
const proxyServer = http2.createServer((req, res) => {
|
|
@@ -12359,14 +13562,26 @@ function parseArgs(args) {
|
|
|
12359
13562
|
}
|
|
12360
13563
|
return parsed2;
|
|
12361
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
|
+
}
|
|
12362
13575
|
if (first === "models" || first === "favorites") {
|
|
12363
13576
|
const parsed2 = emptyParsed("models");
|
|
13577
|
+
parsed2.modelCatalogScope = "global";
|
|
12364
13578
|
for (const arg of rest) {
|
|
12365
13579
|
if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
|
|
12366
13580
|
else if (arg === "--version" || arg === "-v") parsed2.showVersion = true;
|
|
12367
13581
|
else if (arg === "--agy") parsed2.favoritesAgy = true;
|
|
12368
13582
|
else if (!parsed2.error) parsed2.error = `Unknown models option: ${arg}`;
|
|
12369
13583
|
}
|
|
13584
|
+
if (parsed2.favoritesAgy) parsed2.modelCatalogScope = "agy";
|
|
12370
13585
|
return parsed2;
|
|
12371
13586
|
}
|
|
12372
13587
|
if (first === "providers") {
|
|
@@ -12407,6 +13622,16 @@ function parseArgs(args) {
|
|
|
12407
13622
|
parsed2.vertex = true;
|
|
12408
13623
|
continue;
|
|
12409
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
|
+
}
|
|
12410
13635
|
const consumed = tryConsumeRelayLaunchFlag(arg, rest, i, parsed2);
|
|
12411
13636
|
if (consumed !== null) {
|
|
12412
13637
|
if ("error" in consumed) return parsed2;
|
|
@@ -12455,6 +13680,16 @@ function parseArgs(args) {
|
|
|
12455
13680
|
parsed2.vertex = true;
|
|
12456
13681
|
continue;
|
|
12457
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
|
+
}
|
|
12458
13693
|
if (arg === "--help" || arg === "-h") {
|
|
12459
13694
|
parsed2.showHelp = true;
|
|
12460
13695
|
continue;
|
|
@@ -12610,6 +13845,7 @@ ${pc12.bold("Usage:")}
|
|
|
12610
13845
|
relay-ai ui
|
|
12611
13846
|
relay-ai models
|
|
12612
13847
|
relay-ai favorites
|
|
13848
|
+
relay-ai subagents
|
|
12613
13849
|
relay-ai providers
|
|
12614
13850
|
relay-ai --help
|
|
12615
13851
|
relay-ai --version
|
|
@@ -12628,6 +13864,7 @@ ${pc12.bold("Commands:")}
|
|
|
12628
13864
|
claude Launch Claude Code \u2014 pick a provider from your registry
|
|
12629
13865
|
models Manage favorite models for mid-session /model switching (max ${MAX_MODEL_CATALOG})
|
|
12630
13866
|
favorites Alias for models
|
|
13867
|
+
subagents Manage the independent Codex SubAgent model catalog (starts empty)
|
|
12631
13868
|
providers Add, import, and manage your AI providers
|
|
12632
13869
|
server Run a foreground API gateway (OpenCode Zen / Go and local providers)
|
|
12633
13870
|
codex Launch OpenAI Codex CLI with registry providers
|
|
@@ -12635,9 +13872,9 @@ ${pc12.bold("Commands:")}
|
|
|
12635
13872
|
agy Launch Antigravity CLI with registry providers
|
|
12636
13873
|
antigravity Launch Antigravity app with registry providers (macOS)
|
|
12637
13874
|
antigravity-ide Launch Antigravity IDE with registry providers (macOS)
|
|
12638
|
-
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)
|
|
12639
13876
|
chatgpt Alias for codex-app
|
|
12640
|
-
claude-app Launch Claude Desktop app with registry providers (macOS + Windows)
|
|
13877
|
+
claude-app Launch Claude Desktop app with registry providers (macOS + Windows + Linux)
|
|
12641
13878
|
|
|
12642
13879
|
${pc12.bold("Antigravity favorites:")}
|
|
12643
13880
|
agy, antigravity, and antigravity-ide share up to six Antigravity favorites
|
|
@@ -12767,7 +14004,22 @@ ${pc12.bold("Endpoints:")}
|
|
|
12767
14004
|
OpenAI-compatible: OPENAI_BASE_URL=http://127.0.0.1:17645/openai/v1
|
|
12768
14005
|
API key: use anything locally; use the server password in network mode.`;
|
|
12769
14006
|
}
|
|
12770
|
-
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
|
+
}
|
|
12771
14023
|
return `${pc12.bold("relay-ai favorites")} v${VERSION}
|
|
12772
14024
|
Manage favorite models for mid-session switching.
|
|
12773
14025
|
|
|
@@ -12777,6 +14029,7 @@ ${pc12.bold("Usage:")}
|
|
|
12777
14029
|
relay-ai models
|
|
12778
14030
|
relay-ai favorites --help
|
|
12779
14031
|
relay-ai favorites --version
|
|
14032
|
+
relay-ai subagents
|
|
12780
14033
|
|
|
12781
14034
|
${pc12.bold("Behavior:")}
|
|
12782
14035
|
Opens an interactive manager to add or remove favorites.
|
|
@@ -12784,9 +14037,11 @@ ${pc12.bold("Behavior:")}
|
|
|
12784
14037
|
Pick from Zen, Go, or any provider in your registry.
|
|
12785
14038
|
Global favorites are saved to ~/.relay-ai/config.json (max ${MAX_MODEL_CATALOG}).
|
|
12786
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).
|
|
12787
14041
|
|
|
12788
14042
|
${pc12.bold("How it works:")}
|
|
12789
|
-
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.
|
|
12790
14045
|
Favorites appear in supported /model switch menus.
|
|
12791
14046
|
relay-ai agy, antigravity, and antigravity-ide use the Antigravity favorites
|
|
12792
14047
|
list so the limited native switch slots stay predictable: one selected launch
|
|
@@ -12883,9 +14138,9 @@ ${pc12.bold("Examples:")}
|
|
|
12883
14138
|
relay-ai antigravity
|
|
12884
14139
|
relay-ai antigravity --provider zen --model deepseek-v4-flash-free`;
|
|
12885
14140
|
}
|
|
12886
|
-
function printHelp(
|
|
14141
|
+
function printHelp(text5) {
|
|
12887
14142
|
console.log(`
|
|
12888
|
-
${
|
|
14143
|
+
${text5}
|
|
12889
14144
|
`);
|
|
12890
14145
|
}
|
|
12891
14146
|
async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindow, trace, claudeArgs) {
|
|
@@ -12922,15 +14177,19 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
|
|
|
12922
14177
|
var AGY_CLI_FAVORITES_CAP = 6;
|
|
12923
14178
|
async function runModelsCommand(opts = {}) {
|
|
12924
14179
|
const scope = opts.scope ?? "global";
|
|
12925
|
-
const maxFavorites = scope === "agy" ? AGY_CLI_FAVORITES_CAP : MAX_MODEL_CATALOG;
|
|
12926
|
-
const scopeName = scope === "agy" ? "Antigravity CLI Favorites" : "Favorite Models";
|
|
12927
|
-
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";
|
|
12928
14186
|
relayIntro(scopeName);
|
|
12929
14187
|
const spinner9 = p14.spinner();
|
|
12930
14188
|
spinner9.start("Loading providers...");
|
|
12931
14189
|
const catalog = await fetchProviderCatalog();
|
|
12932
14190
|
spinner9.stop("");
|
|
12933
|
-
const
|
|
14191
|
+
const pickedProviders = providersForPicker(catalog);
|
|
14192
|
+
const allProviders = scope === "agy" ? providersForTarget(pickedProviders, "antigravity") : scope === "codex-subagents" ? providersForCodexSubagents(pickedProviders) : pickedProviders;
|
|
12934
14193
|
const favoriteProviders = allProviders.map((provider) => ({
|
|
12935
14194
|
...provider,
|
|
12936
14195
|
name: favoriteProviderDisplayName(provider)
|
|
@@ -12948,7 +14207,7 @@ async function runModelsCommand(opts = {}) {
|
|
|
12948
14207
|
}
|
|
12949
14208
|
}
|
|
12950
14209
|
const prefs = loadPreferences();
|
|
12951
|
-
let favorites = scope === "agy" ? prefs.antigravityCliFavoriteModels ?? [] : prefs.favoriteModels ?? [];
|
|
14210
|
+
let favorites = scope === "agy" ? prefs.antigravityCliFavoriteModels ?? [] : scope === "codex-subagents" ? prefs.codexSubagentModels ?? [] : prefs.favoriteModels ?? [];
|
|
12952
14211
|
let favoritesDirty = false;
|
|
12953
14212
|
while (true) {
|
|
12954
14213
|
const options = [];
|
|
@@ -12962,7 +14221,7 @@ async function runModelsCommand(opts = {}) {
|
|
|
12962
14221
|
options.push({
|
|
12963
14222
|
value: "__add__",
|
|
12964
14223
|
label: atCap ? pc12.dim(`+ Add a model \u2192 (limit of ${maxFavorites} reached)`) : pc12.cyan("+ Add a model \u2192"),
|
|
12965
|
-
hint: atCap ?
|
|
14224
|
+
hint: atCap ? `Remove a ${listItemLabel} first to make room` : `${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""} available`
|
|
12966
14225
|
});
|
|
12967
14226
|
options.push({ value: "__done__", label: "Done", hint: "" });
|
|
12968
14227
|
const header = favorites.length === 0 ? `${scopeName} (0/${maxFavorites})` : `${scopeName} (${favorites.length}/${maxFavorites}) \u2014 select to remove`;
|
|
@@ -12974,16 +14233,16 @@ async function runModelsCommand(opts = {}) {
|
|
|
12974
14233
|
if (p14.isCancel(choice) || choice === "__done__") break;
|
|
12975
14234
|
if (choice === "__add__") {
|
|
12976
14235
|
if (atCap) {
|
|
12977
|
-
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.`);
|
|
12978
14237
|
continue;
|
|
12979
14238
|
}
|
|
12980
14239
|
const globalCount = buildGlobalFavoriteIndex(favoriteProviders).length;
|
|
12981
14240
|
const addPath = await p14.select({
|
|
12982
|
-
message: "Add a favorite",
|
|
14241
|
+
message: subagentScope ? "Add a Codex SubAgent model" : "Add a favorite",
|
|
12983
14242
|
options: [
|
|
12984
14243
|
{
|
|
12985
14244
|
value: "global",
|
|
12986
|
-
label: pc12.cyan("Search all providers"),
|
|
14245
|
+
label: pc12.cyan(subagentScope ? "Search all models" : "Search all providers"),
|
|
12987
14246
|
hint: `${globalCount} models \xB7 ${favoriteProviders.length} provider${favoriteProviders.length !== 1 ? "s" : ""}`
|
|
12988
14247
|
},
|
|
12989
14248
|
{
|
|
@@ -13002,7 +14261,7 @@ async function runModelsCommand(opts = {}) {
|
|
|
13002
14261
|
let provider;
|
|
13003
14262
|
let browsedMultiple = [];
|
|
13004
14263
|
if (addPath === "global") {
|
|
13005
|
-
const globalPick = await pickGlobalFavoriteModel(favoriteProviders, favorites);
|
|
14264
|
+
const globalPick = await pickGlobalFavoriteModel(favoriteProviders, favorites, { listLabel });
|
|
13006
14265
|
if (globalPick === null) continue;
|
|
13007
14266
|
if (globalPick !== ADD_BY_PROVIDER) {
|
|
13008
14267
|
provider = favoriteProviders.find((ap) => ap.id === globalPick.providerId);
|
|
@@ -13010,7 +14269,7 @@ async function runModelsCommand(opts = {}) {
|
|
|
13010
14269
|
}
|
|
13011
14270
|
}
|
|
13012
14271
|
if (addPath === "free") {
|
|
13013
|
-
const globalPick = await pickGlobalFavoriteModel(favoriteProviders, favorites, { freeOnly: true });
|
|
14272
|
+
const globalPick = await pickGlobalFavoriteModel(favoriteProviders, favorites, { freeOnly: true, listLabel });
|
|
13014
14273
|
if (globalPick === null) continue;
|
|
13015
14274
|
if (globalPick !== ADD_BY_PROVIDER) {
|
|
13016
14275
|
provider = favoriteProviders.find((ap) => ap.id === globalPick.providerId);
|
|
@@ -13028,13 +14287,30 @@ async function runModelsCommand(opts = {}) {
|
|
|
13028
14287
|
});
|
|
13029
14288
|
if (p14.isCancel(pickedProviderId)) break;
|
|
13030
14289
|
provider = favoriteProviders.find((ap) => ap.id === pickedProviderId);
|
|
13031
|
-
|
|
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) => {
|
|
13032
14308
|
const favorited = isFavorite(favorites, { providerId: provider.id, modelId: m.id });
|
|
13033
14309
|
const label = formatCodexModelLabel(m);
|
|
13034
14310
|
return {
|
|
13035
14311
|
value: m.id,
|
|
13036
14312
|
label: fmtModel(label, m.id),
|
|
13037
|
-
hint: favorited ? pc12.yellow(
|
|
14313
|
+
hint: favorited ? pc12.yellow(`\u2605 already in ${listLabel}`) : ""
|
|
13038
14314
|
};
|
|
13039
14315
|
});
|
|
13040
14316
|
const pickedModelIds = await p14.multiselect({
|
|
@@ -13050,7 +14326,7 @@ async function runModelsCommand(opts = {}) {
|
|
|
13050
14326
|
currentInitialProvider = provider.id;
|
|
13051
14327
|
continue;
|
|
13052
14328
|
}
|
|
13053
|
-
browsedMultiple =
|
|
14329
|
+
browsedMultiple = modelsToPick.filter((m) => pickedModelIds.includes(m.id));
|
|
13054
14330
|
break;
|
|
13055
14331
|
}
|
|
13056
14332
|
if (browsedMultiple.length === 0) continue;
|
|
@@ -13077,36 +14353,36 @@ async function runModelsCommand(opts = {}) {
|
|
|
13077
14353
|
if (addedModels.length > 0) {
|
|
13078
14354
|
if (addedModels.length === 1) {
|
|
13079
14355
|
const modelName = addedModels[0].name || addedModels[0].id;
|
|
13080
|
-
p14.log.success(`Added ${modelName} (${provider.name}) to
|
|
14356
|
+
p14.log.success(`Added ${modelName} (${provider.name}) to ${listLabel}.`);
|
|
13081
14357
|
} else {
|
|
13082
|
-
p14.log.success(`Added ${addedModels.length} models from ${provider.name} to
|
|
14358
|
+
p14.log.success(`Added ${addedModels.length} models from ${provider.name} to ${listLabel}.`);
|
|
13083
14359
|
}
|
|
13084
14360
|
}
|
|
13085
14361
|
if (duplicateCount > 0) {
|
|
13086
|
-
p14.log.warn(`${duplicateCount} selected model(s) were already in
|
|
14362
|
+
p14.log.warn(`${duplicateCount} selected model(s) were already in ${listLabel}.`);
|
|
13087
14363
|
}
|
|
13088
14364
|
if (limitReached) {
|
|
13089
|
-
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.`);
|
|
13090
14366
|
}
|
|
13091
14367
|
} else if (choice.startsWith("fav-")) {
|
|
13092
14368
|
const idx = parseInt(choice.slice(4), 10);
|
|
13093
14369
|
const fav = favorites[idx];
|
|
13094
14370
|
const entry = modelLookup.get(`${fav.providerId}:${fav.modelId}`);
|
|
13095
14371
|
const label = entry ? `${entry.modelName} (${entry.providerName})` : fav.modelId;
|
|
13096
|
-
const confirmed = await p14.confirm({ message: `Remove ${label} from
|
|
14372
|
+
const confirmed = await p14.confirm({ message: `Remove ${label} from ${listLabel}?` });
|
|
13097
14373
|
if (p14.isCancel(confirmed) || !confirmed) continue;
|
|
13098
14374
|
favorites = removeFavorite(favorites, fav);
|
|
13099
14375
|
favoritesDirty = true;
|
|
13100
|
-
p14.log.success(`Removed ${label} from
|
|
14376
|
+
p14.log.success(`Removed ${label} from ${listLabel}.`);
|
|
13101
14377
|
}
|
|
13102
14378
|
}
|
|
13103
14379
|
if (favoritesDirty) {
|
|
13104
14380
|
savePreferences({ [configKey]: favorites });
|
|
13105
14381
|
}
|
|
13106
|
-
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`;
|
|
13107
14383
|
relayOutro(
|
|
13108
|
-
|
|
13109
|
-
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")
|
|
13110
14386
|
);
|
|
13111
14387
|
return 0;
|
|
13112
14388
|
}
|
|
@@ -13593,7 +14869,7 @@ Options:
|
|
|
13593
14869
|
--trace Write debug logs under ~/.relay-ai/logs/`);
|
|
13594
14870
|
return 0;
|
|
13595
14871
|
}
|
|
13596
|
-
const { runUiCommand } = await import("./ui-command-
|
|
14872
|
+
const { runUiCommand } = await import("./ui-command-FARF2BF4.js");
|
|
13597
14873
|
return runUiCommand({ trace: parsed.trace, serverMode: parsed.uiServerMode });
|
|
13598
14874
|
}
|
|
13599
14875
|
if (parsed.command === "models") {
|
|
@@ -13602,10 +14878,10 @@ Options:
|
|
|
13602
14878
|
return 0;
|
|
13603
14879
|
}
|
|
13604
14880
|
if (parsed.showHelp) {
|
|
13605
|
-
printHelp(modelsHelpText());
|
|
14881
|
+
printHelp(modelsHelpText(parsed.modelCatalogScope === "codex-subagents" ? "codex-subagents" : "global"));
|
|
13606
14882
|
return 0;
|
|
13607
14883
|
}
|
|
13608
|
-
return runModelsCommand({ scope: parsed.favoritesAgy ? "agy" : "global" });
|
|
14884
|
+
return runModelsCommand({ scope: parsed.modelCatalogScope ?? (parsed.favoritesAgy ? "agy" : "global") });
|
|
13609
14885
|
}
|
|
13610
14886
|
if (parsed.command === "providers") {
|
|
13611
14887
|
if (parsed.showVersion) {
|
|
@@ -13636,7 +14912,7 @@ Options:
|
|
|
13636
14912
|
console.log(codexAppHelpText());
|
|
13637
14913
|
return 0;
|
|
13638
14914
|
}
|
|
13639
|
-
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 });
|
|
13640
14916
|
}
|
|
13641
14917
|
if (parsed.command === "claude-app") {
|
|
13642
14918
|
if (parsed.showVersion) {
|
|
@@ -13661,7 +14937,8 @@ Options:
|
|
|
13661
14937
|
return runCodexCommand(parsed.claudeArgs, parsed.trace, {
|
|
13662
14938
|
launchProvider: parsed.launchProvider,
|
|
13663
14939
|
launchModel: parsed.launchModel,
|
|
13664
|
-
vertex: parsed.vertex
|
|
14940
|
+
vertex: parsed.vertex,
|
|
14941
|
+
codexLaunchMode: parsed.codexLaunchMode
|
|
13665
14942
|
});
|
|
13666
14943
|
}
|
|
13667
14944
|
if (parsed.command === "gemini") {
|