@bitkyc08/opencodex 2.57.0 → 2.59.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 (241) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-C5IebErG.js +136 -0
  3. package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +2 -2
  8. package/src/adapters/base.ts +11 -1
  9. package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
  10. package/src/adapters/command-code.ts +13 -4
  11. package/src/adapters/cursor/catalog.ts +11 -0
  12. package/src/adapters/cursor/cursor-errors.ts +15 -0
  13. package/src/adapters/cursor/discovery.ts +65 -1
  14. package/src/adapters/cursor/effort-map.ts +16 -2
  15. package/src/adapters/cursor/envelope-echo.ts +55 -2
  16. package/src/adapters/cursor/live-transport.ts +5 -1
  17. package/src/adapters/cursor/message-mapper.ts +3 -2
  18. package/src/adapters/cursor/protobuf-events.ts +110 -11
  19. package/src/adapters/cursor/protobuf-request.ts +27 -6
  20. package/src/adapters/cursor/request-builder.ts +14 -3
  21. package/src/adapters/cursor/text-toolcall.ts +230 -0
  22. package/src/adapters/cursor/thread-continuity.ts +141 -0
  23. package/src/adapters/cursor/tool-guidance.ts +5 -4
  24. package/src/adapters/cursor/types.ts +5 -0
  25. package/src/adapters/cursor.ts +97 -6
  26. package/src/adapters/devin/cloud-direct/chat.ts +11 -2
  27. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  28. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
  29. package/src/adapters/devin.ts +75 -13
  30. package/src/adapters/google-antigravity-wire.ts +29 -2
  31. package/src/adapters/google-http.ts +45 -13
  32. package/src/adapters/google.ts +23 -4
  33. package/src/adapters/mimo-free.ts +32 -17
  34. package/src/adapters/ollama-native.ts +42 -8
  35. package/src/adapters/openai-chat/response-events.ts +61 -0
  36. package/src/adapters/openai-chat.ts +5 -10
  37. package/src/adapters/openai-responses/passthrough.ts +40 -5
  38. package/src/adapters/openai-responses/request-strips.ts +43 -0
  39. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  40. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  41. package/src/adapters/physical-send.ts +50 -0
  42. package/src/adapters/responses-tool-schema.ts +76 -46
  43. package/src/adapters/run-turn-queue.ts +17 -4
  44. package/src/bridge/response-json.ts +2 -2
  45. package/src/bridge/sse.ts +166 -25
  46. package/src/claude/context-windows.ts +22 -0
  47. package/src/claude/outbound.ts +46 -5
  48. package/src/cli/account-api.ts +4 -3
  49. package/src/cli/account-extended.ts +22 -2
  50. package/src/cli/account-orca-import.ts +63 -0
  51. package/src/cli/account.ts +32 -4
  52. package/src/cli/capabilities.ts +40 -0
  53. package/src/cli/claude.ts +29 -1
  54. package/src/cli/codex-cli-update.ts +97 -2
  55. package/src/cli/config-command.ts +35 -18
  56. package/src/cli/dispatch.ts +71 -4
  57. package/src/cli/doctor.ts +197 -2
  58. package/src/cli/help.ts +4 -1
  59. package/src/cli/index.ts +132 -22
  60. package/src/cli/models-runtime.ts +33 -4
  61. package/src/cli/registry.ts +11 -1
  62. package/src/cli/runtime-api.ts +44 -0
  63. package/src/cli/start-args.ts +94 -0
  64. package/src/cli/system-command.ts +72 -1
  65. package/src/cli/uninstall-client-state.ts +12 -0
  66. package/src/client/machine-api.ts +4 -3
  67. package/src/client/machine-listener.ts +14 -1
  68. package/src/clients/config-export/constants.ts +2 -3
  69. package/src/clients/config-export.ts +5 -5
  70. package/src/codex/account-store.ts +81 -5
  71. package/src/codex/auth-api/pool-quota-probe.ts +14 -3
  72. package/src/codex/auth-api/routes.ts +17 -2
  73. package/src/codex/auth-context.ts +58 -20
  74. package/src/codex/catalog/build-entries.ts +25 -4
  75. package/src/codex/catalog/derive-entry.ts +8 -1
  76. package/src/codex/catalog/effort.ts +10 -6
  77. package/src/codex/catalog/gather-capture.ts +1 -0
  78. package/src/codex/catalog/model-hints.ts +37 -5
  79. package/src/codex/catalog/parsing.ts +83 -5
  80. package/src/codex/catalog/reserve-warn.ts +96 -0
  81. package/src/codex/catalog/retained-sync.ts +19 -0
  82. package/src/codex/catalog/routed-gather.ts +42 -3
  83. package/src/codex/cli-installation-identity.ts +210 -0
  84. package/src/codex/cli-installation-targets.ts +158 -0
  85. package/src/codex/convergence.ts +5 -0
  86. package/src/codex/desktop-switches.ts +145 -0
  87. package/src/codex/history-job.ts +5 -1
  88. package/src/codex/history-provider.ts +37 -5
  89. package/src/codex/history-state-open.ts +105 -0
  90. package/src/codex/history-worker.ts +14 -1
  91. package/src/codex/inject/config-toml.ts +44 -2
  92. package/src/codex/inject/remove.ts +145 -7
  93. package/src/codex/inject/restore.ts +204 -32
  94. package/src/codex/inject.ts +6 -9
  95. package/src/codex/lineage.ts +83 -32
  96. package/src/codex/loopback-target.ts +40 -0
  97. package/src/codex/main-account-hard-lock.ts +2 -1
  98. package/src/codex/main-account.ts +10 -3
  99. package/src/codex/main-device-reauth.ts +17 -9
  100. package/src/codex/model-entitlements.ts +60 -1
  101. package/src/codex/native-profile-startup.ts +64 -20
  102. package/src/codex/observed-model-denials.ts +137 -0
  103. package/src/codex/orca-auth-source.ts +94 -0
  104. package/src/codex/orca-import.ts +219 -0
  105. package/src/codex/prompt-text-probe.ts +282 -12
  106. package/src/codex/quota-401-recovery.ts +12 -0
  107. package/src/codex/quota-types.ts +65 -0
  108. package/src/codex/quota.ts +24 -19
  109. package/src/codex/routing/cooldown-math.ts +8 -47
  110. package/src/codex/routing/pin-drain.ts +57 -0
  111. package/src/codex/routing.ts +13 -15
  112. package/src/codex/subagent-model-fallback.ts +94 -0
  113. package/src/codex/windows-installation-files.ts +224 -0
  114. package/src/combos/failover.ts +122 -5
  115. package/src/config/atomic-write.ts +83 -8
  116. package/src/config/diagnostics.ts +21 -0
  117. package/src/config/load-degrade.ts +15 -0
  118. package/src/config/pending-teardown.ts +8 -0
  119. package/src/config/process-state.ts +36 -3
  120. package/src/config/provider-relative-send-path.ts +16 -0
  121. package/src/config/proxy-env.ts +23 -5
  122. package/src/config/schema/config-schema.ts +23 -0
  123. package/src/config/schema/leaf-validators.ts +65 -17
  124. package/src/generated/compatibility-version.json +337 -201
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/lib/bounded-body.ts +4 -2
  127. package/src/lib/bounded-subprocess.ts +62 -10
  128. package/src/lib/destination-policy.ts +48 -6
  129. package/src/lib/errors.ts +3 -15
  130. package/src/lib/local-destinations.ts +32 -5
  131. package/src/lib/provider-outbound.ts +3 -3
  132. package/src/lib/proxy-env.ts +70 -3
  133. package/src/lib/request-execution-budget.ts +11 -3
  134. package/src/lib/response-body-inactivity.ts +193 -0
  135. package/src/lib/retry-delay.ts +69 -0
  136. package/src/lib/socks5-fetch.ts +631 -0
  137. package/src/lib/spend-reservation-ledger.ts +115 -9
  138. package/src/lib/windows-secret-acl.ts +151 -15
  139. package/src/lib/windows-user-principal.ts +5 -1
  140. package/src/lib/workflow-budget.ts +145 -8
  141. package/src/oauth/account-quota-rank.ts +72 -15
  142. package/src/oauth/generic-account-failover.ts +40 -27
  143. package/src/oauth/orcarouter.ts +15 -2
  144. package/src/oauth/store.ts +8 -0
  145. package/src/providers/codex-capacity.ts +9 -0
  146. package/src/providers/derive.ts +6 -0
  147. package/src/providers/devin-provider-merge-migration.ts +33 -12
  148. package/src/providers/free-directory.ts +20 -2
  149. package/src/providers/key-failover.ts +261 -7
  150. package/src/providers/model-discovery.ts +19 -7
  151. package/src/providers/model-rename-migration.ts +1 -0
  152. package/src/providers/openai-sidecar.ts +4 -0
  153. package/src/providers/opencode-go-transport.ts +14 -5
  154. package/src/providers/quota/report-cache.ts +3 -0
  155. package/src/providers/registry/entries-core.ts +11 -0
  156. package/src/providers/registry/entries-extended.ts +146 -28
  157. package/src/providers/registry/model-seeds.ts +136 -29
  158. package/src/providers/registry/types.ts +9 -0
  159. package/src/responses/apply-patch-envelope.ts +44 -11
  160. package/src/responses/bridge-search-replay-cache.ts +152 -0
  161. package/src/responses/code-mode-helper-compat.ts +26 -16
  162. package/src/responses/custom-tool-compat.ts +1 -1
  163. package/src/responses/hosted-tool-policy.ts +85 -2
  164. package/src/responses/schema.ts +9 -2
  165. package/src/responses/spill-store.ts +17 -0
  166. package/src/responses/state/body-policy.ts +25 -0
  167. package/src/responses/state/spill-queue.ts +8 -6
  168. package/src/responses/state.ts +3 -22
  169. package/src/router.ts +4 -0
  170. package/src/server/auth-cors.ts +27 -0
  171. package/src/server/chat-completions.ts +9 -4
  172. package/src/server/chat-native-sse.ts +26 -9
  173. package/src/server/chat-native.ts +10 -4
  174. package/src/server/claude-messages.ts +24 -2
  175. package/src/server/gui-static.ts +36 -2
  176. package/src/server/inbound-body-admission.ts +187 -0
  177. package/src/server/index/websocket-handler.ts +48 -1
  178. package/src/server/index.ts +15 -19
  179. package/src/server/management/api-access.ts +3 -4
  180. package/src/server/management/config-routes.ts +57 -10
  181. package/src/server/management/provider-capability-config.ts +35 -7
  182. package/src/server/management/provider-routes.ts +70 -18
  183. package/src/server/models-capabilities.ts +24 -3
  184. package/src/server/proxy-liveness.ts +97 -2
  185. package/src/server/relay.ts +17 -24
  186. package/src/server/request-log.ts +25 -1
  187. package/src/server/responses/adapter-continuation.ts +71 -27
  188. package/src/server/responses/adapter-delivery.ts +39 -8
  189. package/src/server/responses/adapter-dispatch.ts +52 -24
  190. package/src/server/responses/codex-ws-exchange.ts +65 -4
  191. package/src/server/responses/combo-stream-preflight.ts +68 -5
  192. package/src/server/responses/compact.ts +60 -11
  193. package/src/server/responses/core-codex-account.ts +83 -22
  194. package/src/server/responses/core-combo.ts +26 -0
  195. package/src/server/responses/core-normalize.ts +12 -5
  196. package/src/server/responses/core-options.ts +3 -0
  197. package/src/server/responses/fetch-helpers.ts +72 -3
  198. package/src/server/responses/native-injection-protocol.ts +42 -0
  199. package/src/server/responses/native-injection-replay.ts +105 -0
  200. package/src/server/responses/native-injection.ts +242 -0
  201. package/src/server/responses/native-response-control.ts +56 -0
  202. package/src/server/responses/native-response-json.ts +14 -0
  203. package/src/server/responses/native-response-output.ts +37 -0
  204. package/src/server/responses/native-steering-log.ts +44 -0
  205. package/src/server/responses/native-steering-policy.ts +49 -0
  206. package/src/server/responses/native-steering-replay.ts +126 -0
  207. package/src/server/responses/native-steering-settings.ts +76 -0
  208. package/src/server/responses/native-steering.ts +400 -0
  209. package/src/server/responses/native-tool-results.ts +130 -0
  210. package/src/server/responses/passthrough-delivery.ts +21 -1
  211. package/src/server/responses/passthrough-dispatch.ts +146 -49
  212. package/src/server/responses/passthrough-execution.ts +11 -1
  213. package/src/server/responses/request-prepare.ts +70 -0
  214. package/src/server/responses/request-send-budget.ts +84 -7
  215. package/src/server/responses/request-sidecar-auth.ts +16 -8
  216. package/src/server/responses/request-spend.ts +38 -9
  217. package/src/server/responses/request-transport.ts +13 -10
  218. package/src/server/responses/run-turn-execution.ts +20 -5
  219. package/src/server/responses/sidecar-execution.ts +2 -0
  220. package/src/server/responses/ws-upstream.ts +23 -2
  221. package/src/server/responses-custom-tool-repair.ts +2 -2
  222. package/src/server/sse-frame-buffer.ts +12 -10
  223. package/src/server/sse-payload-rewrite.ts +36 -9
  224. package/src/server/stop-teardown.ts +8 -1
  225. package/src/server/system-env-shell.ts +5 -1
  226. package/src/server/system-env.ts +7 -1
  227. package/src/server/workflow-refusal.ts +56 -2
  228. package/src/server/ws-bridge.ts +16 -1
  229. package/src/service/cli.ts +29 -7
  230. package/src/service/guards.ts +10 -0
  231. package/src/service/health.ts +43 -0
  232. package/src/service/state.ts +7 -2
  233. package/src/types/accounts.ts +4 -0
  234. package/src/types/config.ts +104 -3
  235. package/src/types/provider.ts +32 -0
  236. package/src/types/request.ts +7 -1
  237. package/src/types/wire.ts +9 -1
  238. package/src/usage/expected-prices.ts +28 -0
  239. package/src/usage/log.ts +87 -4
  240. package/src/web-search/passthrough-bridge.ts +39 -5
  241. package/gui/dist/assets/index-Cz7CLdif.js +0 -128
@@ -295,6 +295,14 @@ export function quarantinePendingTeardown(nonce: string): string | null {
295
295
  * alone. Only an abandoned obligation is a candidate, and a VALID one still has to prove
296
296
  * its endpoint is down before anything is restored — an invalid one never can, which is
297
297
  * what {@link quarantinePendingTeardown} exists for.
298
+ *
299
+ * `isAlive` must answer "is the OWNER still running", which is not "is this PID alive".
300
+ * PIDs are reused: after the owner exits its number can be handed to an unrelated process,
301
+ * and a bare liveness probe then reports the owner as running for as long as that process
302
+ * lives. The receipt is neither recovered nor quarantined nor reported while both updater
303
+ * gates keep refusing on it, which is a permanent fail-closed with no way forward (#4897).
304
+ * `handleStop` therefore passes an identity-aware predicate; do not substitute a bare
305
+ * `process.kill(pid, 0)` here for cheapness.
298
306
  */
299
307
  export function isPendingTeardownAbandoned(
300
308
  read: PendingTeardownRead | TeardownScanFailure,
@@ -110,16 +110,29 @@ export function removeRuntimePortIfPidIs(snapshotPid: number | null): void {
110
110
  try { unlinkSync(getRuntimePortPath()); } catch { /* ignore */ }
111
111
  }
112
112
 
113
- export function isOcxStartCommandLine(commandLine: string): boolean {
113
+ /**
114
+ * Does this command line belong to an opencodex process at all, whatever it is doing?
115
+ *
116
+ * {@link isOcxStartCommandLine} answers the narrower question the pidfile needs — "is this
117
+ * the proxy?" — by additionally requiring the `start` verb. Ownership of a pending-teardown
118
+ * receipt is the broader question: the owner is whichever invocation claimed it, which is an
119
+ * `ocx stop` or the `ocx update` worker that drove it, never an `ocx start`. Asking the
120
+ * start-shaped question there would call every real owner foreign.
121
+ */
122
+ export function isOcxCommandLine(commandLine: string): boolean {
114
123
  const normalized = commandLine.toLowerCase().replace(/\\/g, "/");
115
124
  // Keep legacy source launches and npm's in-place Windows rename recognizable:
116
125
  // a service wrapper may respawn from `.opencodex-*` during a global update.
117
- const hasOcxEntrypoint = normalized.includes("src/cli.ts")
126
+ return normalized.includes("src/cli.ts")
118
127
  || normalized.includes("src/cli/index.ts")
119
128
  || normalized.includes("@bitkyc08/opencodex")
120
129
  || /@bitkyc08\/\.opencodex-/.test(normalized)
121
130
  || /(?:^|[\s/"'])(?:ocx|opencodex)(?:\.cmd)?(?:$|[\s"'])/.test(normalized);
122
- return hasOcxEntrypoint && /(?:^|[\s"'])start(?:$|[\s"'])/.test(normalized);
131
+ }
132
+
133
+ export function isOcxStartCommandLine(commandLine: string): boolean {
134
+ const normalized = commandLine.toLowerCase().replace(/\\/g, "/");
135
+ return isOcxCommandLine(commandLine) && /(?:^|[\s"'])start(?:$|[\s"'])/.test(normalized);
123
136
  }
124
137
 
125
138
  /** Avoid spawning WMIC/PowerShell on every short liveness poll. */
@@ -227,6 +240,26 @@ export function verifyPidIdentity(candidatePid: number): number | null {
227
240
  return isLikelyOcxStartProcess(candidatePid) ? candidatePid : null;
228
241
  }
229
242
 
243
+ /**
244
+ * Is this PID an opencodex process, rather than merely a live one?
245
+ *
246
+ * `process.kill(pid, 0)` answers "does this number name a process", which is not the same
247
+ * question. PIDs are reused, so a recorded owner that exited can have its number handed to
248
+ * an unrelated process, and bare liveness then reports the owner as still running forever.
249
+ *
250
+ * Deliberately uncached, unlike {@link isLikelyOcxStartProcess}. That cache exists because
251
+ * liveness polling asks about the same PID many times a second; this is asked once per
252
+ * outstanding teardown receipt in a short-lived `ocx stop`, so the cache would only add an
253
+ * unswept map. The Windows WMIC/PowerShell probe cost is the point rather than a regression:
254
+ * the runtime contract requires the identity check, not the cheap one, before acting on
255
+ * stale state.
256
+ */
257
+ export function isLikelyOcxProcess(pid: number): boolean {
258
+ const commandLine = readProcessCommandLine(pid);
259
+ if (commandLine === undefined) return false;
260
+ return isOcxCommandLine(commandLine);
261
+ }
262
+
230
263
  type ProcessCommandLineExec = (
231
264
  executable: string,
232
265
  args: string[],
@@ -0,0 +1,16 @@
1
+ /** Shared, initialization-independent shape check for provider send-path overrides. */
2
+ export function providerRelativeSendPathConfigError(
3
+ field: "responsesPath" | "chatCompletionsPath",
4
+ value: unknown,
5
+ ): string | null {
6
+ if (value === undefined) return null;
7
+ if (typeof value !== "string") return `${field} must be a string`;
8
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || value.includes("://")) {
9
+ return `${field} must be a relative path without a URL scheme`;
10
+ }
11
+ if (!value.startsWith("/")) return `${field} must start with /`;
12
+ if (value.includes("?") || value.includes("#")) {
13
+ return `${field} must not include query strings or fragments`;
14
+ }
15
+ return null;
16
+ }
@@ -1,3 +1,5 @@
1
+ import { configureSocks5Fetch } from "../lib/proxy-env";
2
+ import { redactUrlForLog } from "../lib/redact";
1
3
  import { join } from "node:path";
2
4
  import { DEFAULT_SUBAGENT_MODELS, SUBAGENT_MODELS_VERSION } from "./subagent-models";
3
5
  import { MULTI_AGENT_SURFACE_ADVISORY_VERSION } from "./multi-agent-surface";
@@ -102,10 +104,15 @@ function warnProxyConfigDiscardOnce(kind: "proxy" | "noProxy" | "noProxyElements
102
104
  * variables win; config fills missing scheme proxies, which take precedence over ALL_PROXY for WS.
103
105
  * localhost/127.0.0.1 are appended to NO_PROXY so the CLI's own health checks and
104
106
  * running-proxy API calls stay direct. Call once per process entry that makes outbound provider
105
- * requests (server start, catalog sync).
107
+ * requests (server start, catalog sync). `announce` prints the one-line startup banner naming
108
+ * the outbound proxy actually in effect; only server start passes it, because on catalog sync
109
+ * that line is noise. The URL is redacted because a proxy URL can carry credentials.
106
110
  */
107
- export function applyProxyEnv(config: OcxConfig): void {
111
+ export function applyProxyEnv(config: OcxConfig, announce = false): void {
108
112
  applyProxyEnvWith(config);
113
+ if (!announce) return;
114
+ const outbound = process.env.ALL_PROXY?.trim() || process.env.HTTPS_PROXY?.trim() || process.env.HTTP_PROXY?.trim();
115
+ if (outbound) console.log(` outbound proxy: ${redactUrlForLog(outbound)}`);
109
116
  }
110
117
 
111
118
  /** Test seam for `proxy: "auto"`: the registry reader and platform are injectable. */
@@ -123,6 +130,7 @@ export function applyProxyEnvWith(
123
130
  let proxy = typeof rawProxy === "string" ? resolveEnvValue(rawProxy) : undefined;
124
131
  if (!proxy) {
125
132
  if (rawProxy !== undefined) warnProxyConfigDiscardOnce("proxy");
133
+ configureSocks5Fetch();
126
134
  return;
127
135
  }
128
136
  if (proxy.trim().toLowerCase() === "auto") {
@@ -151,8 +159,18 @@ export function applyProxyEnvWith(
151
159
  }
152
160
  }
153
161
  if (proxy) {
154
- if (!process.env.HTTP_PROXY?.trim() && !process.env.http_proxy?.trim()) process.env.HTTP_PROXY = proxy;
155
- if (!process.env.HTTPS_PROXY?.trim() && !process.env.https_proxy?.trim()) process.env.HTTPS_PROXY = proxy;
162
+ if (/^socks/i.test(proxy.trim()) && !/^socks5h?:\/\//i.test(proxy.trim())) {
163
+ throw new Error("Only SOCKS5 proxy URLs are supported; use socks5://host:port");
164
+ }
165
+ if (/^socks5h?:\/\//i.test(proxy.trim())) {
166
+ for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY", "all_proxy"] as const) {
167
+ delete process.env[key];
168
+ }
169
+ process.env.ALL_PROXY = proxy;
170
+ } else {
171
+ if (!process.env.HTTP_PROXY?.trim() && !process.env.http_proxy?.trim()) process.env.HTTP_PROXY = proxy;
172
+ if (!process.env.HTTPS_PROXY?.trim() && !process.env.https_proxy?.trim()) process.env.HTTPS_PROXY = proxy;
173
+ }
156
174
  }
157
175
  const existing = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
158
176
  const entries = existing.split(",").map(s => s.trim()).filter(Boolean);
@@ -184,5 +202,5 @@ export function applyProxyEnvWith(
184
202
  }
185
203
  }
186
204
  process.env.NO_PROXY = entries.join(",");
205
+ configureSocks5Fetch();
187
206
  }
188
-
@@ -13,6 +13,7 @@ import {
13
13
  quotaResetNotifySchema,
14
14
  remoteGuiConfigSchema,
15
15
  runtimeRoleSchema,
16
+ spendSchema,
16
17
  configuredCodexPoolAccountIds,
17
18
  apiKeyEntrySchema,
18
19
  asideProfileSyncSchema,
@@ -59,6 +60,8 @@ import { parseDesktopProfile } from "../../claude/desktop-profile";
59
60
  import { DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES, MAX_APP_OWNED_MEMORY_BUDGET_MB, MIN_APP_OWNED_MEMORY_BUDGET_MB } from "../../lib/app-owned-memory";
60
61
 
61
62
  export const configSchema = z.object({
63
+ codexNativeSteering: z.boolean().optional().catch(false),
64
+ codexNativeInjection: z.boolean().optional().catch(false),
62
65
  port: z.number().int().min(0).max(65535).default(10100),
63
66
  // A malformed hand edit must disable only remote-role behavior, not discard
64
67
  // providers or data-plane keys. Live writes are rejected explicitly below.
@@ -164,6 +167,11 @@ export const configSchema = z.object({
164
167
  quotaResetNotify: quotaResetNotifySchema.optional().catch(undefined),
165
168
  // Same rationale: a bad auto-refresh section must not cost the operator their providers.
166
169
  catalogAutoRefresh: catalogAutoRefreshSchema.optional().catch(undefined),
170
+ // Same rationale again, with the failure direction stated: a malformed spend section
171
+ // degrades to "no ceiling", which means observe-only accounting rather than an outage. That
172
+ // is the safe degrade for traffic and the dangerous one for the operator, so the write path
173
+ // rejects it and loadConfig warns -- the same pair codexPool uses below.
174
+ spend: spendSchema.optional().catch(undefined),
167
175
  // These selections pre-date schema validation and used to pass through as
168
176
  // unknown fields. Invalid hand edits must disable only the optional
169
177
  // delegation/native-default feature, not reject the whole config and hide
@@ -180,6 +188,10 @@ export const configSchema = z.object({
180
188
  codexShimAutoRestore: z.boolean().optional(),
181
189
  codexDesktopAuthless: z.boolean().optional().catch(undefined),
182
190
  codexClientCompaction: z.boolean().optional().catch(undefined),
191
+ // Presentation-only label for the injected provider. A malformed value degrades to undefined
192
+ // and the default label is emitted, rather than failing the parse or writing a config Codex
193
+ // would refuse to load — the provider id routing depends on is never derived from it.
194
+ codexProviderDisplayName: z.string().trim().min(1).max(128).optional().catch(undefined),
183
195
  pausedCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional(),
184
196
  // A malformed policy degrades to "no policy" rather than failing the parse, so a hand-edited
185
197
  // typo cannot trip the backup-and-defaults repair path and wipe providers or pool accounts.
@@ -462,6 +474,17 @@ export const configSchema = z.object({
462
474
  message: reasoningSummariesError,
463
475
  });
464
476
  }
477
+ const suppressSyntheticMaxError = booleanRecordConfigError(
478
+ (provider as { modelSuppressSyntheticMax?: unknown }).modelSuppressSyntheticMax,
479
+ "modelSuppressSyntheticMax",
480
+ );
481
+ if (suppressSyntheticMaxError) {
482
+ ctx.addIssue({
483
+ code: "custom",
484
+ path: ["providers", redactSecretString(name), "modelSuppressSyntheticMax"],
485
+ message: suppressSyntheticMaxError,
486
+ });
487
+ }
465
488
  const verbositySupportError = booleanRecordConfigError(
466
489
  (provider as { modelSupportsVerbosity?: unknown }).modelSupportsVerbosity,
467
490
  "modelSupportsVerbosity",
@@ -32,7 +32,11 @@ import { getProviderRegistryEntry, providerMatchesRegistryTransport, providerMod
32
32
  import { resolveOpenAiVirtualModel } from "../../providers/openai-virtual-models";
33
33
  import { COST4_RATE_KEYS, isValidCost4Rate } from "../../usage/user-cost-overlays";
34
34
  import { MAX_COST4_RATE } from "../../usage/expected-prices";
35
- import { isHostedToolUnsupportedForModel } from "../../responses/hosted-tool-policy";
35
+ import {
36
+ DECLARABLE_HOSTED_TOOL_TYPES,
37
+ declaredUnsupportedHostedTools,
38
+ isHostedToolUnsupportedForModel,
39
+ } from "../../responses/hosted-tool-policy";
36
40
  import { getConfigDir } from "../paths";
37
41
 
38
42
  /** One definition of "usable secret", shared by the schema and the warnings. */
@@ -243,10 +247,12 @@ export const providerConfigSchema = z.object({
243
247
  chatCompletionsPath: z.string().min(1).optional(),
244
248
  statelessResponses: z.boolean().optional(),
245
249
  requiresAdjacentResponsesToolResults: z.boolean().optional(),
250
+ requiresPairedResponsesToolResults: z.boolean().optional(),
246
251
  annotateEmptyToolOutputs: z.boolean().optional(),
247
252
  fastWire: fastWireSchema.nullable().optional(),
248
253
  supportsServiceTier: z.boolean().optional(),
249
254
  modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
255
+ modelSuppressSyntheticMax: z.record(z.string().min(1), z.boolean()).optional(),
250
256
  preserveResponsesReasoningContent: z.boolean().optional(),
251
257
  decodesNativeCompactionBlobs: z.boolean().optional(),
252
258
  allowEncryptedV2AgentTasks: z.boolean().optional(),
@@ -274,6 +280,18 @@ export const providerConfigSchema = z.object({
274
280
  omitReasoningEffortWithToolsModels: z.array(z.string().min(1))
275
281
  .transform(normalizeNonBlankStringArray)
276
282
  .optional(),
283
+ // Validated against a closed vocabulary rather than accepted as free strings. This
284
+ // schema ends in `.passthrough()`, so a misspelled `web_serch` would otherwise be
285
+ // accepted, persisted, and strip nothing -- leaving the operator with the upstream 400
286
+ // the field was set to prevent, and no message saying why (the `codexToolMode` lesson,
287
+ // #2106).
288
+ unsupportedHostedTools: z.array(z.string().min(1))
289
+ .transform(normalizeNonBlankStringArray)
290
+ .refine(
291
+ tools => tools.every(tool => DECLARABLE_HOSTED_TOOL_TYPES.has(tool)),
292
+ { message: `unsupportedHostedTools accepts only hosted tool types: ${[...DECLARABLE_HOSTED_TOOL_TYPES].join(", ")}` },
293
+ )
294
+ .optional(),
277
295
  retryOn429: retryOn429PolicySchema.optional(),
278
296
  transientRetryOn5xx: transientRetryOn5xxPolicySchema.optional(),
279
297
  codexAccountMode: z.enum(["pool", "direct"]).optional(),
@@ -299,21 +317,7 @@ export const providerConfigSchema = z.object({
299
317
  }).passthrough();
300
318
 
301
319
 
302
- /**
303
- * Shared shape check for the two relative send-path overrides. `field` names the
304
- * offending key so the message stays specific to what the user actually wrote.
305
- */
306
- export function providerRelativeSendPathConfigError(field: string, value: string | undefined): string | null {
307
- if (value === undefined) return null;
308
- if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || value.includes("://")) {
309
- return `${field} must be a relative path without a URL scheme`;
310
- }
311
- if (!value.startsWith("/")) return `${field} must start with /`;
312
- if (value.includes("?") || value.includes("#")) {
313
- return `${field} must not include query strings or fragments`;
314
- }
315
- return null;
316
- }
320
+ export { providerRelativeSendPathConfigError } from "../provider-relative-send-path";
317
321
 
318
322
  /**
319
323
  * Validate `providers.<name>.modelCosts`: a plain object keyed by exact model
@@ -390,7 +394,13 @@ export function modelPreferHostedToolsConfigError(
390
394
  value: unknown,
391
395
  field: string,
392
396
  providerName: string,
393
- provider: { adapter?: unknown; authMode?: unknown; modelAdapters?: unknown; baseUrl?: unknown },
397
+ provider: {
398
+ adapter?: unknown;
399
+ authMode?: unknown;
400
+ modelAdapters?: unknown;
401
+ baseUrl?: unknown;
402
+ unsupportedHostedTools?: unknown;
403
+ },
394
404
  ): string | null {
395
405
  if (value === undefined) return null;
396
406
  if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;
@@ -398,6 +408,12 @@ export function modelPreferHostedToolsConfigError(
398
408
  if (prototype !== Object.prototype && prototype !== null) return `${field} must be a plain object with own properties`;
399
409
  const entries = Object.entries(value);
400
410
  const registry = getProviderRegistryEntry(providerName);
411
+ // A provider that denies a hosted tool cannot also prefer it. Both keys are
412
+ // provider-owned capability statements about the same tool, and the denial wins at
413
+ // request time, so accepting the pair would silently ignore the preference.
414
+ const declaredUnsupported = declaredUnsupportedHostedTools(
415
+ provider as { unsupportedHostedTools?: readonly string[] },
416
+ );
401
417
  // Effective transport: a `preserveCustomDestination` registry row reused under a
402
418
  // different endpoint keeps its own adapter AND its own auth at runtime, because
403
419
  // `routedProviderConfig()` honors `providerMatchesRegistryTransport()`. Both the
@@ -458,6 +474,9 @@ export function modelPreferHostedToolsConfigError(
458
474
  if (typeof tool !== "string" || !SUPPORTED_PREFERRED_HOSTED_TOOLS.has(tool)) {
459
475
  return `${field}.${key} supports only image_generation`;
460
476
  }
477
+ if (declaredUnsupported.has(tool)) {
478
+ return `${field}.${key} cannot prefer ${tool}: unsupportedHostedTools declares it unsupported`;
479
+ }
461
480
  if (isHostedToolUnsupportedForModel(key, tool)) {
462
481
  return `${field}.${key} cannot prefer ${tool}: the model does not support it`;
463
482
  }
@@ -853,3 +872,32 @@ export const catalogAutoRefreshSchema = z.object({
853
872
  enabled: z.boolean().optional(),
854
873
  intervalMinutes: z.number().int().min(0).max(1440).optional(),
855
874
  }).strict();
875
+
876
+ /**
877
+ * One spend scope's ceiling.
878
+ *
879
+ * `.strict()` for the usual reason and one sharper one. Elsewhere a silently ignored key
880
+ * leaves a feature off that the operator believed was on; here it leaves a BUDGET off, and a
881
+ * budget nobody is enforcing looks exactly like a budget nobody has exceeded. That is #2106 --
882
+ * an undeclared option accepted, persisted, and then read as its default -- aimed at spend.
883
+ *
884
+ * Only positive integers: 0 would read as "no tokens at all" and refuse every request under
885
+ * the scope, which is never what writing a budget means. An operator who wants no ceiling
886
+ * removes the key.
887
+ */
888
+ const spendScopeSchema = z.object({
889
+ maxTokens: z.number().int().positive().optional(),
890
+ }).strict();
891
+
892
+ /**
893
+ * Durable spend ceilings (#4546).
894
+ *
895
+ * Absent, empty, and all-scopes-absent are the same thing: observe-only accounting. There is
896
+ * no default ceiling anywhere in this section, deliberately.
897
+ */
898
+ export const spendSchema = z.object({
899
+ root: spendScopeSchema.optional(),
900
+ identity: spendScopeSchema.optional(),
901
+ pool: spendScopeSchema.optional(),
902
+ retentionDays: z.number().int().min(1).max(365).optional(),
903
+ }).strict();