@bitkyc08/opencodex 2.55.0 → 2.57.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.
Files changed (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -587,8 +587,14 @@ export async function inspectCodexCliInstall(
587
587
  const platform = deps.platform ?? process.platform;
588
588
  const candidate = observeCodexRuntimeCandidateReadOnly(deps);
589
589
  if (!candidate) {
590
+ // This slice reads no candidate or configuration file on Windows, so an
591
+ // absent proof-captured environment candidate does not establish that no
592
+ // Codex CLI exists: a persisted selection is simply never consulted there.
593
+ // Report the deferral that actually happened instead of the stronger claim
594
+ // that the candidate is unavailable. POSIX retains its existing result
595
+ // when no candidate is observed.
590
596
  return isWindowsPlatform(platform)
591
- ? unknownWindowsReport("candidate_unavailable")
597
+ ? unknownWindowsReport("windows_inspection_deferred")
592
598
  : unknownReport("candidate_unavailable");
593
599
  }
594
600
 
@@ -49,7 +49,7 @@ import {
49
49
  orderForSubagents,
50
50
  } from "./catalog/sync";
51
51
  import { multiAgentV2EnabledFromConfigText } from "./features";
52
- import { exactComboCatalogSlugs } from "./catalog/aggregation";
52
+ import { enforceCatalogSlugUniqueness, exactComboCatalogSlugs } from "./catalog/aggregation";
53
53
  import {
54
54
  isNativeAliasCatalogEntry,
55
55
  accountBoundNativeOpenAiSlugs,
@@ -386,7 +386,12 @@ function prepareCatalog(
386
386
  : null,
387
387
  );
388
388
  finalizeAutoReviewModelOverride(mergedModels, catalogModels, config);
389
- catalog.models = mergedModels;
389
+ // The second writer of this file. A dashboard model toggle, a combo edit, or a Codex account
390
+ // login reaches `convergeCodexCatalog` and commits through `fixedCommit`, never through
391
+ // `writeRetainedCatalogSync`, so the #4730 uniqueness guard has to stand here too or the same
392
+ // `source-invalid` rejection returns by a different route. Silent because this merge runs under
393
+ // `warningPolicy: "suppress"`.
394
+ catalog.models = enforceCatalogSlugUniqueness(mergedModels, false);
390
395
  return catalog;
391
396
  }
392
397
 
@@ -116,11 +116,20 @@ export interface DesktopAppAdapter {
116
116
  *
117
117
  * `root` is expected to be `realpath`-resolved by discovery already.
118
118
  */
119
+ function isMembershipSeparator(character: string): boolean {
120
+ // `/` separates on every platform this runs on, and Windows accepts it wherever it
121
+ // accepts `\`. `\` is only a separator where the host says so: it is a legal
122
+ // FILENAME character on POSIX, so admitting it there would reopen the sibling hole
123
+ // this function exists to close.
124
+ return character === "/" || (sep === "\\" && character === "\\");
125
+ }
126
+
119
127
  export function isUnderRoot(executable: string, root: string): boolean {
120
128
  if (!executable || !root) return false;
121
129
  if (executable === root) return true;
122
- const prefix = root.endsWith(sep) ? root : root + sep;
123
- return executable.startsWith(prefix);
130
+ if (!executable.startsWith(root)) return false;
131
+ if (isMembershipSeparator(root[root.length - 1]!)) return true;
132
+ return isMembershipSeparator(executable[root.length] ?? "");
124
133
  }
125
134
 
126
135
  /**
@@ -29,16 +29,16 @@ const SHELL_BASENAME = "chatgpt.exe";
29
29
  const POWERSHELL_PROBE_OPTIONS = { timeout: PROBE_TIMEOUT_MS, windowsHide: true } as const;
30
30
 
31
31
  /**
32
- * isUnderRoot prefixes with the host path.sep and is case-sensitive. Windows
32
+ * isUnderRoot checks a lexical path boundary and is case-sensitive. Windows
33
33
  * membership is case-insensitive, and this file is executed by Unix CI against
34
- * backslash paths, so both sides are folded onto the host separator first.
34
+ * mixed slash paths, so both slash forms are folded onto the host separator first.
35
35
  * The boundary itself — sibling `OpenAI.Codex-evil` must not match root
36
36
  * `OpenAI.Codex` — is still isUnderRoot's, which is why the PowerShell
37
37
  * StartsWith is only a cheap pre-filter.
38
38
  */
39
39
  function toHostMembershipPath(windowsPath: string): string {
40
40
  const lowered = windowsPath.toLowerCase();
41
- return sep === "\\" ? lowered : lowered.replaceAll("\\", "/");
41
+ return lowered.replace(/[\\/]/g, sep);
42
42
  }
43
43
 
44
44
  function isMemberExecutable(executable: string, root: string): boolean {
@@ -91,10 +91,10 @@ function listPackageProcesses(exec: DesktopExec, install: DesktopAppInstall): De
91
91
  const literal = install.root.replace(/'/g, "''");
92
92
  const script = [
93
93
  "$ErrorActionPreference='SilentlyContinue'",
94
- `$root = '${literal}'`,
94
+ `$root = '${literal}'.Replace('/', '\\')`,
95
95
  "$me = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name",
96
96
  "Get-CimInstance Win32_Process -Filter \"Name='ChatGPT.exe'\" |",
97
- " Where-Object { $_.ExecutablePath -and $_.ExecutablePath.StartsWith($root, 'OrdinalIgnoreCase') } |",
97
+ " Where-Object { $_.ExecutablePath -and $_.ExecutablePath.Replace('/', '\\').StartsWith($root, 'OrdinalIgnoreCase') } |",
98
98
  " ForEach-Object {",
99
99
  " $o = Invoke-CimMethod -InputObject $_ -MethodName GetOwner",
100
100
  " if ($o -and $o.ReturnValue -eq 0 -and $o.User) {",
@@ -0,0 +1,563 @@
1
+ // Holds INV-TOML-01 from structure/overview.md; keep the id here if this file is split or renamed.
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { contextCompatibleBaseLine } from "../context-compat";
4
+ import { resolveEffectiveProjectModelProvider } from "../project-config-warnings";
5
+ import {
6
+ OCX_SECTION_MARKER,
7
+ REALTIME_WS_BASE_URL_KEY,
8
+ isRootOpenaiBaseUrlLine,
9
+ isRootRealtimeWsBaseUrlLine,
10
+ tomlStringPattern,
11
+ } from "../injected-marker";
12
+ import {
13
+ CODEX_CONFIG_PATH,
14
+ DEFAULT_CATALOG_PATH,
15
+ parseTomlString,
16
+ resolveCodexConfigPath,
17
+ tomlString,
18
+ } from "../paths";
19
+ import {
20
+ type CodexRoutingTarget,
21
+ providerBaseHost,
22
+ routingTargetOrigin,
23
+ usesProviderTable,
24
+ validateCodexRoutingTarget,
25
+ } from "./routing-target";
26
+
27
+ export function externalCodexModelProvider(content: string): string | null {
28
+ const provider = resolveEffectiveProjectModelProvider(content).provider;
29
+ return provider && provider !== "openai" && provider !== "opencodex"
30
+ ? provider
31
+ : null;
32
+ }
33
+
34
+ export function currentExternalCodexModelProvider(): string | null {
35
+ if (!existsSync(CODEX_CONFIG_PATH)) return null;
36
+ return externalCodexModelProvider(readFileSync(CODEX_CONFIG_PATH, "utf8"));
37
+ }
38
+
39
+ /**
40
+ * Detect the file's dominant line ending. Every transform in this module is LF-pure
41
+ * (split("\n") + hard "\n" joins), so CRLF configs (Windows-edited config.toml) are
42
+ * normalized to LF at the pipeline boundary and converted back on write — otherwise a
43
+ * single inject would leave a mixed-EOL file.
44
+ */
45
+ export function dominantEol(content: string): "\r\n" | "\n" {
46
+ const crlf = (content.match(/\r\n/g) ?? []).length;
47
+ if (crlf === 0) return "\n";
48
+ const bareLf = (content.match(/\n/g) ?? []).length - crlf;
49
+ return crlf >= bareLf ? "\r\n" : "\n";
50
+ }
51
+
52
+ /** Normalize all line endings to `eol` (CRLF first collapsed to LF, then expanded). */
53
+ export function applyEol(content: string, eol: "\r\n" | "\n"): string {
54
+ const lf = content.replace(/\r\n/g, "\n");
55
+ return eol === "\n" ? lf : lf.replace(/\n/g, "\r\n");
56
+ }
57
+
58
+ export function buildProviderTableBlock(
59
+ port: number,
60
+ supportsWebsockets?: boolean,
61
+ includeApiAuthHeader?: boolean,
62
+ hostname?: string,
63
+ ): string;
64
+ export function buildProviderTableBlock(
65
+ target: CodexRoutingTarget,
66
+ supportsWebsockets?: boolean,
67
+ ): string;
68
+ export function buildProviderTableBlock(
69
+ portOrTarget: number | CodexRoutingTarget,
70
+ supportsWebsockets = false,
71
+ includeApiAuthHeader = false,
72
+ hostname?: string,
73
+ ): string {
74
+ const target = typeof portOrTarget === "number"
75
+ ? validateCodexRoutingTarget({
76
+ baseUrl: `http://${providerBaseHost(hostname)}:${portOrTarget}/v1`,
77
+ requiresAdmissionToken: includeApiAuthHeader,
78
+ tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
79
+ })
80
+ : validateCodexRoutingTarget(portOrTarget);
81
+ return buildProviderTableBlockForTarget(target, supportsWebsockets);
82
+ }
83
+
84
+ export function buildProviderTableBlockForTarget(
85
+ target: CodexRoutingTarget,
86
+ supportsWebsockets = false,
87
+ ): string {
88
+ const lines = [
89
+ "",
90
+ OCX_SECTION_MARKER,
91
+ "[model_providers.opencodex]",
92
+ 'name = "OpenCodex Proxy"',
93
+ `base_url = ${tomlString(target.baseUrl)}`,
94
+ 'wire_api = "responses"',
95
+ // false only in the authless Desktop opt-in (#1107); true keeps the App/TUI account gate.
96
+ `requires_openai_auth = ${target.desktopAuthless === true ? "false" : "true"}`,
97
+ ];
98
+ if (target.requiresAdmissionToken) {
99
+ // codex-cli 0.146+ contract (#2073): env_key sends Authorization: Bearer $VAR and
100
+ // hard-errors on a missing/empty variable instead of silently omitting auth. It
101
+ // coexists with requires_openai_auth (env_key wins wire auth; the flag keeps the
102
+ // login/account UX), and the server substitutes stored main auth for our admission
103
+ // bearer (#1686), so the modern form is strictly better than the legacy
104
+ // env_http_headers table this line used to emit.
105
+ lines.push(`env_key = ${tomlString(target.tokenEnv)}`);
106
+ }
107
+ if (supportsWebsockets) lines.push("supports_websockets = true");
108
+ return lines.join("\n") + "\n";
109
+ }
110
+
111
+ export function buildOpenaiBaseUrlLine(
112
+ port: number,
113
+ hostname?: string,
114
+ ): string;
115
+ export function buildOpenaiBaseUrlLine(target: CodexRoutingTarget): string;
116
+ export function buildOpenaiBaseUrlLine(
117
+ portOrTarget: number | CodexRoutingTarget,
118
+ hostname?: string,
119
+ ): string {
120
+ return typeof portOrTarget === "number"
121
+ ? `openai_base_url = "http://${providerBaseHost(hostname)}:${portOrTarget}/v1"`
122
+ : buildOpenaiBaseUrlLineForTarget(validateCodexRoutingTarget(portOrTarget));
123
+ }
124
+
125
+ function buildOpenaiBaseUrlLineForTarget(target: CodexRoutingTarget): string {
126
+ return `openai_base_url = ${tomlString(target.baseUrl)}`;
127
+ }
128
+
129
+ /**
130
+ * Realtime sideband override (codex-rs `experimental_realtime_ws_base_url`), written with the
131
+ * SAME value as `openai_base_url`. Desktop voice creates its WebRTC call through the proxy
132
+ * (`POST /v1/live`, answered under the Pool account the proxy selects) but, since openai/codex
133
+ * 438c9e98d (#35830), joins the sideband at `wss://api.openai.com/v1/live/{callId}` with the
134
+ * app's own login unless this key redirects it. Two accounts, one call: the join 404s. Pointing
135
+ * the key at the proxy sends the join through `GET /v1/live/{callId}` (src/server/live.ts),
136
+ * where the same Pool account is reused. codex-rs turns `http` into `ws` and appends
137
+ * `/live/{callId}` itself; the value must stay the canonical `/v1` root.
138
+ */
139
+ export function buildRealtimeWsBaseUrlLine(target: CodexRoutingTarget): string {
140
+ return `${REALTIME_WS_BASE_URL_KEY} = ${tomlString(target.baseUrl)}`;
141
+ }
142
+
143
+ /**
144
+ * Design B root-key injection: place `OCX_SECTION_MARKER` + `openai_base_url` at the document
145
+ * ROOT (before the first table header). Idempotent: an existing marker-owned line is rewritten
146
+ * in place. A user's OWN root `openai_base_url` (no marker above it) is respected — we keep it
147
+ * and inject nothing, reporting `keptUserBaseUrl` so the caller can surface it.
148
+ */
149
+ export function setRootOpenaiBaseUrl(
150
+ content: string,
151
+ port: number,
152
+ hostname?: string,
153
+ ): { content: string; keptUserBaseUrl: boolean };
154
+ export function setRootOpenaiBaseUrl(
155
+ content: string,
156
+ target: CodexRoutingTarget,
157
+ ): { content: string; keptUserBaseUrl: boolean };
158
+ export function setRootOpenaiBaseUrl(
159
+ content: string,
160
+ portOrTarget: number | CodexRoutingTarget,
161
+ hostname?: string,
162
+ ): { content: string; keptUserBaseUrl: boolean } {
163
+ if (typeof portOrTarget !== "number") {
164
+ return setRootOpenaiBaseUrlForTarget(content, validateCodexRoutingTarget(portOrTarget));
165
+ }
166
+ const lines = content.split("\n");
167
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
168
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
169
+ const key = contextCompatibleBaseLine(content, buildOpenaiBaseUrlLine(portOrTarget, hostname));
170
+
171
+ for (let i = 0; i < rootEnd; i++) {
172
+ if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
173
+ const markerOwned = i > 0 && lines[i - 1].includes(OCX_SECTION_MARKER);
174
+ if (!markerOwned) return { content, keptUserBaseUrl: true };
175
+ lines[i] = key;
176
+ return { content: lines.join("\n"), keptUserBaseUrl: false };
177
+ }
178
+
179
+ if (firstTable === -1) {
180
+ return {
181
+ content:
182
+ content.replace(/\n+$/, "") +
183
+ "\n" +
184
+ OCX_SECTION_MARKER +
185
+ "\n" +
186
+ key +
187
+ "\n",
188
+ keptUserBaseUrl: false,
189
+ };
190
+ }
191
+ let insertAt = firstTable;
192
+ while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
193
+ lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
194
+ return { content: lines.join("\n"), keptUserBaseUrl: false };
195
+ }
196
+
197
+ export function setRootOpenaiBaseUrlForTarget(
198
+ content: string,
199
+ target: CodexRoutingTarget,
200
+ ): { content: string; keptUserBaseUrl: boolean } {
201
+ const lines = content.split("\n");
202
+ const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
203
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
204
+ const key = contextCompatibleBaseLine(content, buildOpenaiBaseUrlLineForTarget(target));
205
+ for (let index = 0; index < rootEnd; index += 1) {
206
+ if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
207
+ const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
208
+ if (!markerOwned) return { content, keptUserBaseUrl: true };
209
+ lines[index] = key;
210
+ return { content: lines.join("\n"), keptUserBaseUrl: false };
211
+ }
212
+ if (firstTable === -1) {
213
+ return {
214
+ content: `${content.replace(/\n+$/, "")}\n${OCX_SECTION_MARKER}\n${key}\n`,
215
+ keptUserBaseUrl: false,
216
+ };
217
+ }
218
+ let insertAt = firstTable;
219
+ while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt -= 1;
220
+ lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
221
+ return { content: lines.join("\n"), keptUserBaseUrl: false };
222
+ }
223
+
224
+ /**
225
+ * Companion to `setRootOpenaiBaseUrlForTarget` for the realtime sideband override. Same
226
+ * ownership rule, applied per key: the line is ours only when the marker sits directly
227
+ * above it; a user's own line (no marker above it) is kept and nothing is injected. The
228
+ * key gets its OWN marker line rather than sharing the routing override's, so a user line
229
+ * that happens to sit right under our `openai_base_url` is never mistaken for ours.
230
+ * Placement: directly after the marker-owned `openai_base_url` pair. Only ever called on
231
+ * the Design B (loopback) path right after the routing override was written — the legacy
232
+ * provider-table form needs the admission-token header, which the sideband cannot carry.
233
+ */
234
+ export function setRootRealtimeWsBaseUrl(
235
+ content: string,
236
+ target: CodexRoutingTarget,
237
+ ): { content: string; keptUserRealtimeWsBaseUrl: boolean } {
238
+ const lines = content.split("\n");
239
+ const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
240
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
241
+ const key = buildRealtimeWsBaseUrlLine(validateCodexRoutingTarget(target));
242
+ for (let index = 0; index < rootEnd; index += 1) {
243
+ if (!isRootRealtimeWsBaseUrlLine(lines[index])) continue;
244
+ const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
245
+ if (!markerOwned) return { content, keptUserRealtimeWsBaseUrl: true };
246
+ lines[index] = key;
247
+ return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
248
+ }
249
+ for (let index = 0; index < rootEnd; index += 1) {
250
+ if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
251
+ if (!(index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER))) continue;
252
+ lines.splice(index + 1, 0, OCX_SECTION_MARKER, key);
253
+ return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
254
+ }
255
+ // No marker-owned routing override to attach to: the override has no owner, so inject nothing.
256
+ return { content, keptUserRealtimeWsBaseUrl: false };
257
+ }
258
+
259
+ /**
260
+ * Remove the marker-owned root `openai_base_url` (marker line + the key line right after it).
261
+ * A user's own root override (no marker) survives; an orphaned marker with no key line after
262
+ * it is dropped too so repeated strip/inject cycles cannot accumulate marker comments.
263
+ * A marker-owned `experimental_realtime_ws_base_url` pair is removed by the same rule.
264
+ */
265
+ export function stripInjectedOpenaiBaseUrl(content: string): string {
266
+ const lines = content.split("\n");
267
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
268
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
269
+ const drop = new Set<number>();
270
+ for (let i = 0; i < rootEnd; i++) {
271
+ if (!lines[i].includes(OCX_SECTION_MARKER)) continue;
272
+ if (i + 1 < rootEnd && (isRootOpenaiBaseUrlLine(lines[i + 1]) || isRootRealtimeWsBaseUrlLine(lines[i + 1]))) {
273
+ drop.add(i);
274
+ drop.add(i + 1);
275
+ } else if (i + 1 >= rootEnd || lines[i + 1].trim() === "") {
276
+ drop.add(i); // orphaned marker at root
277
+ }
278
+ }
279
+ if (drop.size === 0) return content;
280
+ return lines.filter((_, i) => !drop.has(i)).join("\n");
281
+ }
282
+
283
+ /**
284
+ * Strip every existing `model_provider` line that we must not duplicate: any line set to
285
+ * "opencodex" (wherever it sits — including a previously mis-nested one under a table), plus any
286
+ * ROOT-level model_provider (before the first table) of any value, since we override the global.
287
+ * A `model_provider` legitimately inside a user table/profile with a non-opencodex value is left
288
+ * untouched.
289
+ */
290
+ export function stripExistingModelProvider(content: string): string {
291
+ const lines = content.split("\n");
292
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
293
+ const out: string[] = [];
294
+ lines.forEach((line, i) => {
295
+ if (/^\s*model_provider\s*=/.test(line)) {
296
+ const isOurs = /^\s*model_provider\s*=\s*"opencodex"\s*$/.test(line);
297
+ const isRoot = firstTable === -1 || i < firstTable;
298
+ if (isOurs || isRoot) return; // drop it
299
+ }
300
+ out.push(line);
301
+ });
302
+ return out.join("\n");
303
+ }
304
+
305
+ /**
306
+ * Drop ROOT-level `model_context_window` overrides (keys before the first table header). Codex
307
+ * treats this root key as a global override that wins over the per-model catalog values, so a stale
308
+ * `model_context_window = 1000000` makes every model (e.g. gpt-5.5) report a 1M window. User-owned
309
+ * compaction limits do not alter the advertised context window and must survive reinjection.
310
+ */
311
+ export function stripRootContextWindowOverrides(content: string): string {
312
+ const lines = content.split("\n");
313
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
314
+ return lines
315
+ .filter((line, i) => {
316
+ const isRoot = firstTable === -1 || i < firstTable;
317
+ return !isRoot || !/^\s*model_context_window\s*=/.test(line);
318
+ })
319
+ .join("\n");
320
+ }
321
+
322
+ export function stripRootRoutedModel(content: string): string {
323
+ const lines = content.split("\n");
324
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
325
+ return lines
326
+ .filter((line, i) => {
327
+ const isRoot = firstTable === -1 || i < firstTable;
328
+ if (!isRoot) return true;
329
+ const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"\\])*"|'[^']*')\s*$/);
330
+ if (!m) return true;
331
+ const model = parseTomlString(m[1]);
332
+ return !model?.includes("/");
333
+ })
334
+ .join("\n");
335
+ }
336
+
337
+ /**
338
+ * Insert `model_provider = "opencodex"` at the document ROOT — immediately before the first table
339
+ * header (TOML root keys must precede all tables). If there are no tables, append it to the root body.
340
+ */
341
+ export function setRootModelProvider(content: string): string {
342
+ const lines = content.split("\n");
343
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
344
+ const key = 'model_provider = "opencodex"';
345
+ if (firstTable === -1) {
346
+ return content.replace(/\n+$/, "") + "\n" + key + "\n";
347
+ }
348
+ let insertAt = firstTable;
349
+ while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
350
+ lines.splice(insertAt, 0, key);
351
+ return lines.join("\n");
352
+ }
353
+
354
+ function readRootModelCatalogPath(content: string): string | null {
355
+ const lines = content.split("\n");
356
+ const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
357
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
358
+ const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
359
+ let ownedCatalogPath: string | null = null;
360
+ for (let index = 0; index < rootEnd; index += 1) {
361
+ const match = modelCatalogAssignment.exec(lines[index]);
362
+ if (!match) continue;
363
+ const catalogPath = parseTomlString(match[1]);
364
+ if (!isOpencodexCatalogPath(catalogPath)) return catalogPath;
365
+ ownedCatalogPath ??= catalogPath;
366
+ }
367
+ return ownedCatalogPath;
368
+ }
369
+
370
+ export function setRootModelCatalogPath(content: string, catalogPath: string): string {
371
+ const lines = content.split("\n");
372
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
373
+ const key = `model_catalog_json = ${tomlString(catalogPath)}`;
374
+ const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
375
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
376
+ const ownedAssignments: number[] = [];
377
+ let hasUserAssignment = false;
378
+ for (let i = 0; i < rootEnd; i++) {
379
+ const m = modelCatalogAssignment.exec(lines[i]);
380
+ if (!m) continue;
381
+ const existing = parseTomlString(m[1]);
382
+ if (isOpencodexCatalogPath(existing)) {
383
+ ownedAssignments.push(i);
384
+ } else {
385
+ hasUserAssignment = true;
386
+ }
387
+ }
388
+ if (hasUserAssignment) {
389
+ const owned = new Set(ownedAssignments);
390
+ return lines.filter((_, index) => !owned.has(index)).join("\n");
391
+ }
392
+ if (ownedAssignments.length > 0) {
393
+ lines[ownedAssignments[0]] = key;
394
+ const duplicates = new Set(ownedAssignments.slice(1));
395
+ return lines.filter((_, index) => !duplicates.has(index)).join("\n");
396
+ }
397
+ if (firstTable === -1) {
398
+ return content.replace(/\n+$/, "") + "\n" + key + "\n";
399
+ }
400
+ let insertAt = firstTable;
401
+ while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
402
+ lines.splice(insertAt, 0, key);
403
+ return lines.join("\n");
404
+ }
405
+
406
+ export function removeProfileSection(content: string): string {
407
+ const lines = content.split("\n");
408
+ const filtered: string[] = [];
409
+ let inProfile = false;
410
+ for (const line of lines) {
411
+ if (line.trim() === "[profiles.opencodex]") {
412
+ inProfile = true;
413
+ continue;
414
+ }
415
+ if (inProfile) {
416
+ if (/^\s*\[/.test(line) && line.trim() !== "[profiles.opencodex]") {
417
+ inProfile = false;
418
+ filtered.push(line);
419
+ }
420
+ continue;
421
+ }
422
+ filtered.push(line);
423
+ }
424
+ return (
425
+ filtered
426
+ .join("\n")
427
+ .replace(/\n{3,}/g, "\n\n")
428
+ .trimEnd() + "\n"
429
+ );
430
+ }
431
+
432
+ export function normalizeServiceTier(content: string): string {
433
+ return content.replace(
434
+ /^(\s*service_tier\s*=\s*)["']priority["']\s*$/gm,
435
+ '$1"fast"',
436
+ );
437
+ }
438
+
439
+ export function ensureFastModeFeature(content: string, fastMode?: boolean): string {
440
+ // Tri-state fast mode (see OcxConfig.fastMode): true forces `fast_mode = true`,
441
+ // false forces `fast_mode = false`, and undefined leaves the user's config
442
+ // untouched (no [features] table is added and an existing fast_mode line is
443
+ // preserved as-is). Table and key matching accept the valid TOML spellings
444
+ // `[features] # comment`, `["features"]` / `['features']`, and quoted keys.
445
+ const lines = content.split("\n");
446
+ const featuresHeader = /^\s*\[(["']?)\s*features\s*\1\]\s*(?:#.*)?$/;
447
+ const fastModeKey = /^\s*(?:"fast_mode"|'fast_mode'|fast_mode)\s*=/;
448
+ const featuresStart = lines.findIndex(line => featuresHeader.test(line));
449
+ if (featuresStart === -1) {
450
+ if (fastMode === undefined) return content;
451
+ return content.trimEnd() + "\n\n[features]\nfast_mode = " + (fastMode ? "true" : "false") + "\n";
452
+ }
453
+
454
+ const nextTable = lines.findIndex(
455
+ (line, index) => index > featuresStart && /^\s*\[/.test(line),
456
+ );
457
+ const featuresEnd = nextTable === -1 ? lines.length : nextTable;
458
+ for (let i = featuresStart + 1; i < featuresEnd; i++) {
459
+ if (fastModeKey.test(lines[i])) {
460
+ if (fastMode === undefined) return lines.join("\n");
461
+ lines[i] = lines[i].replace(/^(\s*)(?:"fast_mode"|'fast_mode'|fast_mode)\s*=.*$/, `$1fast_mode = ${fastMode ? "true" : "false"}`);
462
+ return lines.join("\n");
463
+ }
464
+ }
465
+
466
+ if (fastMode === undefined) return lines.join("\n");
467
+ let insertAt = featuresEnd;
468
+ while (insertAt > featuresStart + 1 && lines[insertAt - 1].trim() === "") insertAt--;
469
+ lines.splice(insertAt, 0, `fast_mode = ${fastMode ? "true" : "false"}`);
470
+ return lines.join("\n");
471
+ }
472
+
473
+ function isOpencodexCatalogPath(path: string): boolean {
474
+ return path.replace(/\\/g, "/").split("/").pop() === "opencodex-catalog.json";
475
+ }
476
+
477
+ export function stripOpencodexCatalogPath(content: string): string {
478
+ const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
479
+ const lines = content.split("\n");
480
+ const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
481
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
482
+ return lines
483
+ .filter((line, index) => {
484
+ if (index >= rootEnd) return true;
485
+ const m = modelCatalogAssignment.exec(line);
486
+ return !m || !isOpencodexCatalogPath(parseTomlString(m[1]));
487
+ })
488
+ .join("\n");
489
+ }
490
+
491
+ export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets?: boolean, includeApiAuthHeader?: boolean, hostname?: string, fastMode?: boolean): string;
492
+ export function buildProfileFile(target: CodexRoutingTarget, catalogPath?: string | null, supportsWebsockets?: boolean, fastMode?: boolean): string;
493
+ export function buildProfileFile(
494
+ portOrTarget: number | CodexRoutingTarget,
495
+ catalogPath?: string | null,
496
+ supportsWebsockets = false,
497
+ includeApiAuthHeaderOrFastMode?: boolean,
498
+ hostname?: string,
499
+ fastMode?: boolean,
500
+ ): string {
501
+ const target = typeof portOrTarget === "number"
502
+ ? validateCodexRoutingTarget({
503
+ baseUrl: `http://${providerBaseHost(hostname)}:${portOrTarget}/v1`,
504
+ requiresAdmissionToken: includeApiAuthHeaderOrFastMode === true,
505
+ tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
506
+ })
507
+ : validateCodexRoutingTarget(portOrTarget);
508
+ return buildProfileFileForTarget(
509
+ target,
510
+ catalogPath,
511
+ supportsWebsockets,
512
+ typeof portOrTarget === "number" ? fastMode : includeApiAuthHeaderOrFastMode,
513
+ );
514
+ }
515
+
516
+ export function buildProfileFileForTarget(
517
+ target: CodexRoutingTarget,
518
+ catalogPath?: string | null,
519
+ supportsWebsockets = false,
520
+ fastMode?: boolean,
521
+ ): string {
522
+ const origin = routingTargetOrigin(target);
523
+ const host = new URL(origin).host;
524
+ // Design B (loopback): the reference/fallback file documents the root override form.
525
+ // Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
526
+ // the x-opencodex-api-key env header); explicit Desktop policies share that shape.
527
+ if (!usesProviderTable(target)) {
528
+ const lines = [
529
+ "# OpenCodex proxy fallback config (Design B)",
530
+ `# Root override that points Codex's built-in openai provider at the proxy on ${host}.`,
531
+ "# Merge these root keys into ~/.codex/config.toml manually if auto-injection was removed.",
532
+ buildOpenaiBaseUrlLineForTarget(target),
533
+ ];
534
+ if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
535
+ if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`, "");
536
+ return lines.join("\n");
537
+ }
538
+ const lines = [
539
+ "# OpenCodex proxy profile — use with: codex --profile opencodex",
540
+ `# Routes all model requests through the opencodex proxy at ${host}`,
541
+ 'model_provider = "opencodex"',
542
+ ];
543
+ if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
544
+ if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
545
+ lines.push(buildProviderTableBlockForTarget(target, supportsWebsockets).trimEnd(), "");
546
+ return lines.join("\n");
547
+ }
548
+
549
+ export function chooseCatalogPathForInjection(
550
+ content: string,
551
+ requested?: string | null,
552
+ ): string | null {
553
+ if (requested !== undefined) return requested;
554
+
555
+ const existing = readRootModelCatalogPath(content);
556
+ if (existing) {
557
+ const resolved = resolveCodexConfigPath(existing);
558
+ if (!isOpencodexCatalogPath(resolved) || existsSync(resolved))
559
+ return existing;
560
+ }
561
+
562
+ return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
563
+ }