@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/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-5DDQJSTU.js";
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("already in favorites") : ""
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):` : `Search all providers (${index.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 your favorites.`);
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 text4 = messageText(item.content);
2151
- if (text4.trim()) developerParts.push(text4.trim());
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, text4) {
2286
+ function makeReasoningOutputItem(id, text5) {
2225
2287
  return {
2226
2288
  id,
2227
2289
  type: "reasoning",
2228
- summary: text4.trim() ? [{ type: "summary_text", text: text4 }] : []
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 text4 = messageText(item.content);
2337
- messages.push({ role, content: [{ type: "text", text: text4 }] });
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: tool3.args
2912
+ arguments: normalizedArgs
2823
2913
  });
2824
- const fcItem = buildFinalToolItem(resolveOutputKind(tool3.name, options?.toolContext), tool3.name, tool3.callId, tool3.itemId, tool3.args);
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(text4, maxChars) {
3139
- if (text4.length <= maxChars) return text4;
3634
+ function clipTextForContext(text5, maxChars) {
3635
+ if (text5.length <= maxChars) return text5;
3140
3636
  const marker = `
3141
3637
 
3142
- [... ${text4.length} chars clipped from oversized context item ...]
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 `${text4.slice(0, edge)}${marker}${text4.slice(-edge)}`;
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" && url === "/health") {
3869
+ if (req.method === "GET" && effectivePath === "/health") {
3321
3870
  sendJson(res, 200, { ok: true });
3322
3871
  return;
3323
3872
  }
3324
- if (req.method === "GET" && url === "/v1/models") {
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" && url.startsWith("/v1/models/")) {
3351
- const id = url.slice("/v1/models/".length);
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" && url === "/v1/responses") {
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
- let resolved = resolveModel(routes, models, modelId);
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
- body,
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 createHash("sha1").update(clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64");
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
- payloadLen = Number(buf.readBigUInt64BE(2));
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 (opcode !== 1) return null;
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(text4) {
3583
- const payload = Buffer.from(text4, "utf8");
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
- return Buffer.from([136, 0]);
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
- let resolved = resolveModel(routes, models, modelId);
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
- body,
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(), getSessionLockPath(env)];
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 (route.tier === "proxy" && proxyPort) {
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
- const favoritesActive = favorites.length > 0 && !launchPlan.skip;
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 (favoritesActive && resolvedFavorites.length > 0) {
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 (favoritesActive && resolvedFavorites.length > 0) {
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 join5 } from "path";
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
- join5(process.env["APPDATA"] ?? homedir5(), "npm", "gemini.cmd"),
5398
- join5(process.env["APPDATA"] ?? homedir5(), "npm", "gemini")
6468
+ join6(process.env["APPDATA"] ?? homedir5(), "npm", "gemini.cmd"),
6469
+ join6(process.env["APPDATA"] ?? homedir5(), "npm", "gemini")
5399
6470
  ] : [
5400
- join5(homedir5(), ".local", "bin", "gemini"),
5401
- join5(homedir5(), ".npm", "bin", "gemini"),
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(join5(tmpdir(), "relay-ai-gemini-"));
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 = join5(cliHome, ".gemini");
6501
+ const geminiDir = join6(cliHome, ".gemini");
5431
6502
  mkdirSync2(geminiDir);
5432
- writeFileSync2(join5(geminiDir, "settings.json"), `${JSON.stringify(settings, null, 2)}
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(text4) {
5650
- return text4.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");
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 text4 = stripGeminiIdentity(p15.text);
5669
- if (text4.includes("<thinking>")) {
5670
- const tokens = text4.split(/<thinking>([\s\S]*?)<\/thinking>/);
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: text4 });
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 text4 = `Current model: ${current.displayName} (${current.aliasId})
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, text4, isStream, current.aliasId);
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 text4 = p15.textDelta ?? p15.text ?? "";
7003
+ let text5 = p15.textDelta ?? p15.text ?? "";
5933
7004
  if (!isThinking) {
5934
7005
  isThinking = true;
5935
- text4 = `<thinking>
5936
- ` + text4;
7006
+ text5 = `<thinking>
7007
+ ` + text5;
5937
7008
  }
5938
7009
  const chunk = {
5939
- candidates: [{ content: { role: "model", parts: [{ text: text4 }] } }],
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 text4 = p15.textDelta ?? p15.text ?? "";
7017
+ let text5 = p15.textDelta ?? p15.text ?? "";
5947
7018
  if (isThinking) {
5948
7019
  isThinking = false;
5949
- text4 = `
7020
+ text5 = `
5950
7021
  </thinking>
5951
7022
 
5952
- ` + text4;
7023
+ ` + text5;
5953
7024
  }
5954
7025
  const chunk = {
5955
7026
  candidates: [{
5956
7027
  content: {
5957
7028
  role: "model",
5958
- parts: [{ text: text4 }]
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 text4 = parts[0]?.text;
6142
- if (typeof text4 !== "string") return null;
6143
- const trimmed = text4.trim();
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, text4, isStream, modelVersion) {
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, text4, modelVersion);
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: text4 }] },
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, text4, modelVersion) {
7241
+ function writeGeminiStreamText(res, text5, modelVersion) {
6171
7242
  const chunk = {
6172
7243
  candidates: [{
6173
- content: { role: "model", parts: [{ text: text4 }] },
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, text4, startSse) {
7991
- if (!text4) return;
9061
+ function emitThinkingDelta(res, route, responseId, text5, startSse) {
9062
+ if (!text5) return;
7992
9063
  startSse();
7993
9064
  const chunk = formatCloudCodeChunk({
7994
- thought: text4,
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(text4, tag) {
8003
- for (let len = Math.min(tag.length - 1, text4.length); len > 0; len--) {
8004
- if (text4.endsWith(tag.slice(0, len))) return len;
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 text4 = "";
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
- text4 += src.slice(0, src.length - len);
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
- text4 += src.slice(0, idx);
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: text4 };
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(text4, knownToolNames) {
8110
- const trimmed = text4.trim();
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: text4 } = thinkFilter(reasoningDeltaText(p15));
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 (text4) {
8205
- log14(`[gateway] text-delta: ${JSON.stringify(text4.slice(0, 500))}`);
8206
- if (!bufferingJsonText && (textBuffer + text4).trimStart().startsWith("{")) {
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 += text4;
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: text4,
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 execFileSync2, execSync as execSync3 } from "child_process";
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 join6 } from "path";
9484
+ import { join as join7 } from "path";
8414
9485
  var isWindows4 = process.platform === "win32";
8415
9486
  var FALLBACK_PATHS = isWindows4 ? [
8416
- join6(process.env["APPDATA"] ?? homedir6(), "npm", "agy.cmd"),
8417
- join6(process.env["APPDATA"] ?? homedir6(), "npm", "agy"),
8418
- join6(homedir6(), "AppData", "Roaming", "npm", "agy.cmd")
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
- join6(homedir6(), ".local", "bin", "agy"),
8421
- join6(homedir6(), ".npm", "bin", "agy"),
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 = execFileSync2(binaryPath, ["--version"], {
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 execFileSync3, execSync as execSync4, spawn as spawn5 } from "child_process";
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 join7 } from "path";
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 = join7(homedir7(), ".relay-ai", "antigravity", "app-profile");
8534
- var LINUX_IDE_PROFILE_DIR = join7(homedir7(), ".relay-ai", "antigravity", "profile");
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
- join7(homedir7(), ".local", "share", "antigravity", "antigravity")
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 execFileSync3("ps", psArgs, {
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
- execFileSync3("osascript", ["-e", 'tell application "Antigravity IDE" to quit'], {
9735
+ execFileSync4("osascript", ["-e", 'tell application "Antigravity IDE" to quit'], {
8665
9736
  stdio: ["ignore", "pipe", "pipe"]
8666
9737
  });
8667
9738
  } catch {
8668
- execFileSync3("osascript", ["-e", 'tell application id "com.google.antigravity-ide" to quit'], {
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
- execFileSync3("osascript", ["-e", 'tell application "Antigravity" to quit'], {
9755
+ execFileSync4("osascript", ["-e", 'tell application "Antigravity" to quit'], {
8685
9756
  stdio: ["ignore", "pipe", "pipe"]
8686
9757
  });
8687
9758
  } catch {
8688
- execFileSync3("osascript", ["-e", 'tell application id "com.google.antigravity" to quit'], {
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"] ?? join7(homedir7(), "AppData", "Local");
8698
- const winPath = join7(localAppData, "Programs", "Antigravity", "Antigravity.exe");
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 = join7(homedir7(), "Applications", "Antigravity.app", "Contents", "MacOS", "Antigravity");
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"] ?? join7(homedir7(), "AppData", "Local");
8714
- const winPath = join7(localAppData, "Programs", "Antigravity IDE", "Antigravity IDE.exe");
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 = join7(homedir7(), "Applications", "Antigravity IDE.app", "Contents", "Resources", "app", "bin", "antigravity-ide");
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 = join7(homedir7(), ".relay-ai", "antigravity", "extensions");
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 join8 } from "path";
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 = join8(homedir8(), ".relay-ai", "antigravity", "app-profile");
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 = join8(homedir8(), ".relay-ai", "antigravity", "profile");
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 join9 } from "path";
10361
+ import { dirname as dirname2, join as join10 } from "path";
9289
10362
  import { parse, stringify } from "smol-toml";
9290
10363
  function getCodexConfigPath() {
9291
- return join9(getCodexHome(), "config.toml");
10364
+ return join10(getCodexHome(), "config.toml");
9292
10365
  }
9293
10366
  function getCodexAppSidecarProfilePath() {
9294
- return join9(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
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(text4) {
9321
- if (!text4.trim()) return {};
9322
- return asRecord(parse(text4));
10397
+ function parseCodexConfig(text5) {
10398
+ if (!text5.trim()) return {};
10399
+ return asRecord(parse(text5));
9323
10400
  }
9324
- function captureRestoreState(text4) {
9325
- const config = parseCodexConfig(text4);
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(text4) {
9354
- const config = parseCodexConfig(text4);
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(text4, spec) {
9413
- const config = parseCodexConfig(text4);
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
- if (baseUrl.value !== `http://127.0.0.1:${spec.proxyPort}/v1`) {
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 text4 = `${stringify(merged)}
10538
+ const text5 = `${stringify(merged)}
9444
10539
  `;
9445
- validateAppConfigText(text4, spec);
10540
+ validateAppConfigText(text5, spec);
9446
10541
  mkdirSync3(dirname2(configPath), { recursive: true });
9447
- writeFileSync3(configPath, text4, "utf8");
9448
- return text4;
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 text4 = `${stringify(buildCodexAppRootConfig(spec))}
10603
+ const text5 = `${stringify(buildCodexAppRootConfig(spec))}
9501
10604
  `;
9502
- validateAppConfigText(text4, spec);
9503
- return text4;
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 join10 } from "path";
10619
+ import { basename as basename2, join as join11 } from "path";
9516
10620
  function getAppSessionLockPath(env = process.env) {
9517
- return join10(getRelayAiCodexDir(env), "session-app.json");
10621
+ return join11(getRelayAiCodexDir(env), "session-app.json");
9518
10622
  }
9519
10623
  function getAppRestoreStatePath(env = process.env) {
9520
- return join10(getRelayAiCodexDir(env), "app-restore-state.json");
10624
+ return join11(getRelayAiCodexDir(env), "app-restore-state.json");
9521
10625
  }
9522
10626
  function getAppCatalogPath(providerId, env = process.env) {
9523
- return join10(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
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 = join10(backupsDir, `${base}.${Date.now()}.bak`);
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 text4 = readCodexConfigText();
10677
+ const text5 = readCodexConfigText();
9574
10678
  const existing = readAppRestoreState(env);
9575
- if (existing && isAppManagedConfig(text4)) {
10679
+ if (existing && isAppManagedConfig(text5)) {
9576
10680
  return existing;
9577
10681
  }
9578
- const state = captureRestoreState(text4);
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) => join10(codexDir, 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 text4 = readCodexConfigText();
9608
- const managed = isAppManagedConfig(text4);
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 Windows. Linux is not supported (no ChatGPT desktop app).
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
- const favoritesActive = favorites.length > 0;
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 = favoritesActive && resolvedFavorites.length > 0 ? {
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 (favoritesActive) {
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 (favoritesActive && resolvedFavorites.length > 0) {
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 join11, dirname as dirname3 } from "path";
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 join11(process.env.LOCALAPPDATA || join11(homedir9(), "AppData", "Local"), "Claude-3p");
11425
+ return join13(process.env.LOCALAPPDATA || join13(homedir9(), "AppData", "Local"), "Claude-3p");
10233
11426
  }
10234
11427
  if (process.platform === "linux") {
10235
- return join11(process.env.XDG_CONFIG_HOME || join11(homedir9(), ".config"), "Claude-3p");
11428
+ return join13(process.env.XDG_CONFIG_HOME || join13(homedir9(), ".config"), "Claude-3p");
10236
11429
  }
10237
- return join11(homedir9(), "Library", "Application Support", "Claude-3p");
11430
+ return join13(homedir9(), "Library", "Application Support", "Claude-3p");
10238
11431
  }
10239
11432
  function getConfigLibraryPath() {
10240
- return join11(getClaudeDesktopHome(), "configLibrary");
11433
+ return join13(getClaudeDesktopHome(), "configLibrary");
10241
11434
  }
10242
11435
  function getMetaJsonPath() {
10243
- return join11(getConfigLibraryPath(), "_meta.json");
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 = join11(getConfigLibraryPath(), `${uuid}.json`);
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 join12 } from "path";
11629
+ import { dirname as dirname4, join as join14 } from "path";
10437
11630
  function getSessionLockPath2() {
10438
- return join12(getClaudeDesktopHome(), ".relay-ai.lock");
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 = join12(getConfigLibraryPath(), `${uuid}.json`);
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 join13 } from "path";
12005
+ import { join as join15 } from "path";
10813
12006
  var SKILL_DIR_NAME = "relay-ai-cli";
10814
12007
  var SKILL_INSTALL_DIRS = [
10815
- join13(getAppHome(), "skills"),
10816
- join13(homedir10(), ".claude", "skills"),
10817
- join13(homedir10(), ".agents", "skills"),
10818
- join13(homedir10(), ".codex", "skills"),
10819
- join13(homedir10(), ".cursor", "skills"),
10820
- join13(homedir10(), ".cursor", "skills-cursor")
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 = join13(skillDir, "SKILL.md");
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 = join13(dir, SKILL_DIR_NAME);
10848
- return { skillDir, skillPath: join13(skillDir, "SKILL.md") };
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 statSync2,
12712
+ statSync as statSync3,
11510
12713
  writeFileSync as writeFileSync8
11511
12714
  } from "fs";
11512
- import { dirname as dirname6, join as join15, resolve } from "path";
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 = randomBytes(16);
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 = join15(appHome, SESSION_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 = join15(root, name);
12739
+ const sessionDir = join17(root, name);
11537
12740
  try {
11538
- const stat = statSync2(sessionDir);
12741
+ const stat = statSync3(sessionDir);
11539
12742
  if (!stat.isDirectory()) continue;
11540
- const ownerPath = join15(sessionDir, OWNER_FILE);
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 = statSync2(ownerPath);
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 = join15(appHome, SESSION_ROOT);
12766
+ const root = join17(appHome, SESSION_ROOT);
11564
12767
  mkdirSync9(root, { recursive: true, mode: 448 });
11565
- chmodSync(root, 448);
11566
- const sessionDir = join15(root, randomUUID3());
12768
+ chmodSync2(root, 448);
12769
+ const sessionDir = join17(root, randomUUID3());
11567
12770
  mkdirSync9(sessionDir, { mode: 448 });
11568
- chmodSync(sessionDir, 448);
11569
- writeFileSync8(join15(sessionDir, OWNER_FILE), `${process.pid}
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 = join15(sessionDir, "relay-ai-ca.pem");
12807
+ const caCertPath = join17(sessionDir, "relay-ai-ca.pem");
11605
12808
  writeFileSync8(caCertPath, caCert, { encoding: "utf8", mode: 384 });
11606
- chmodSync(caCertPath, 384);
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 = join15(dirname6(relayCaCertPath), "combined-ca.pem");
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
- chmodSync(combinedPath, 384);
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 randomBytes2, timingSafeEqual } from "crypto";
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 = randomBytes2(32).toString("base64url");
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(text4) {
14141
+ function printHelp(text5) {
12887
14142
  console.log(`
12888
- ${text4}
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 configKey = scope === "agy" ? "antigravityCliFavoriteModels" : "favoriteModels";
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 allProviders = scope === "agy" ? providersForTarget(providersForPicker(catalog), "antigravity") : providersForPicker(catalog);
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 ? "Remove a favorite first to make room" : `${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""} available`
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
- const options2 = provider.models.map((m) => {
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("\u2605 already favorite") : ""
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 = provider.models.filter((m) => pickedModelIds.includes(m.id));
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 favorites.`);
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 favorites.`);
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 your favorites.`);
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 favorites?` });
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 favorites.`);
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 favLabel = scope === "agy" ? "Antigravity CLI " : "";
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
- favorites.length === 0 ? `No ${favLabel}favorites saved` : `${favorites.length} ${favLabel}favorite${favorites.length !== 1 ? "s" : ""} saved`,
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-RYWL36VR.js");
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") {