@jacobbd/relay-ai 0.7.6 → 0.9.0

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