@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -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
+ }