@bitkyc08/opencodex 2.86.0 → 2.87.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 (378) hide show
  1. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
  4. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic.ts +108 -21
  14. package/src/adapters/base.ts +2 -0
  15. package/src/adapters/claude-cli/adapter.ts +111 -23
  16. package/src/adapters/claude-cli/profiles.ts +2 -1
  17. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  18. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  19. package/src/adapters/coding-agent/protocol.ts +1 -1
  20. package/src/adapters/coding-agent/turn.ts +18 -2
  21. package/src/adapters/dsh-account.ts +97 -0
  22. package/src/adapters/google.ts +35 -8
  23. package/src/adapters/openai-chat/messages.ts +4 -3
  24. package/src/adapters/openai-chat/passthrough.ts +8 -2
  25. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  26. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  27. package/src/adapters/openai-responses/passthrough.ts +57 -125
  28. package/src/adapters/openai-responses/request-strips.ts +25 -3
  29. package/src/adapters/openai-responses/transport.ts +81 -0
  30. package/src/adapters/qoder/adapter.ts +4 -2
  31. package/src/adapters/registry.ts +5 -0
  32. package/src/adapters/zed.ts +26 -1
  33. package/src/advisor/activity.ts +55 -0
  34. package/src/advisor/budget.ts +62 -0
  35. package/src/advisor/consult.ts +4 -1
  36. package/src/advisor/context.ts +16 -4
  37. package/src/advisor/evidence-tool.ts +21 -0
  38. package/src/advisor/evidence.ts +117 -0
  39. package/src/advisor/runtime.ts +79 -31
  40. package/src/advisor/settings.ts +11 -2
  41. package/src/bridge/errors.ts +19 -1
  42. package/src/bridge/internal.ts +3 -1
  43. package/src/bridge/response-json.ts +18 -3
  44. package/src/bridge/sse.ts +58 -8
  45. package/src/bridge/terminal-refusal.ts +59 -0
  46. package/src/claude/desktop-3p-library.ts +32 -7
  47. package/src/claude/desktop-3p.ts +55 -13
  48. package/src/claude/desktop-first-party.ts +1 -1
  49. package/src/claude/desktop-picker-platform.ts +4 -0
  50. package/src/claude/desktop-picker.ts +15 -8
  51. package/src/claude/desktop-system-proxy.ts +50 -4
  52. package/src/claude/inbound-content-options.ts +11 -0
  53. package/src/claude/inbound.ts +33 -0
  54. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  55. package/src/claude/intercept/picker-runtime.ts +6 -5
  56. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  57. package/src/claude/intercept/picker-trust.ts +19 -0
  58. package/src/claude/intercept/port-policy.ts +30 -0
  59. package/src/claude/intercept/runtime.ts +97 -94
  60. package/src/claude/outbound.ts +38 -13
  61. package/src/cli/account-api.ts +1 -0
  62. package/src/cli/account-policy-dto.ts +6 -0
  63. package/src/cli/account-policy.ts +34 -5
  64. package/src/cli/account-preservation.ts +44 -0
  65. package/src/cli/account.ts +8 -2
  66. package/src/cli/advisor.ts +17 -3
  67. package/src/cli/capabilities-accounts.ts +20 -1
  68. package/src/cli/capabilities-agents-routing.ts +5 -2
  69. package/src/cli/capabilities-base.ts +38 -10
  70. package/src/cli/capabilities-observe-system.ts +63 -3
  71. package/src/cli/capabilities-provider-models.ts +11 -0
  72. package/src/cli/codex-cli-update.ts +151 -41
  73. package/src/cli/codex-provider.ts +44 -0
  74. package/src/cli/combo-input.ts +10 -1
  75. package/src/cli/combo-result.ts +1 -1
  76. package/src/cli/combo.ts +2 -1
  77. package/src/cli/cross-home-owner.ts +17 -14
  78. package/src/cli/dispatch.ts +25 -4
  79. package/src/cli/doctor.ts +6 -1
  80. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  81. package/src/cli/help.ts +3 -2
  82. package/src/cli/index.ts +1 -1
  83. package/src/cli/integration-plan-dto.ts +1 -0
  84. package/src/cli/log-history.ts +172 -0
  85. package/src/cli/models-runtime-subcommands.ts +1 -0
  86. package/src/cli/models-runtime.ts +72 -8
  87. package/src/cli/observe-stream.ts +2 -0
  88. package/src/cli/observe.ts +16 -4
  89. package/src/cli/provider.ts +27 -5
  90. package/src/cli/registry.ts +28 -8
  91. package/src/cli/runtime-api.ts +5 -3
  92. package/src/cli/system-command.ts +40 -8
  93. package/src/cli/uninstall-client-state.ts +32 -0
  94. package/src/cli/usage-compare.ts +309 -0
  95. package/src/cli/usage-model-search.ts +4 -4
  96. package/src/cli/usage-report.ts +2 -2
  97. package/src/cli/v2.ts +1 -1
  98. package/src/client/machine-listener.ts +1 -3
  99. package/src/client/runtime.ts +1 -0
  100. package/src/clients/client-path.ts +51 -0
  101. package/src/clients/config-export/contracts.ts +2 -1
  102. package/src/clients/config-export.ts +18 -44
  103. package/src/clients/gentle-shell-path.ts +87 -0
  104. package/src/codex/account-auto-switch.ts +2 -0
  105. package/src/codex/account-identity.ts +29 -0
  106. package/src/codex/account-label.ts +1 -1
  107. package/src/codex/account-lifecycle.ts +21 -5
  108. package/src/codex/account-preservation.ts +28 -0
  109. package/src/codex/account-use-remaining.ts +23 -0
  110. package/src/codex/app-server-processes.ts +97 -7
  111. package/src/codex/app-server-restart-service.ts +55 -4
  112. package/src/codex/auth-api/account-list.ts +4 -0
  113. package/src/codex/auth-api/account-pause-group.ts +6 -50
  114. package/src/codex/auth-api/account-preservation.ts +57 -0
  115. package/src/codex/auth-api/routes.ts +8 -0
  116. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  117. package/src/codex/auth-api.ts +0 -1
  118. package/src/codex/auth-context.ts +2 -2
  119. package/src/codex/catalog/auto-review.ts +20 -3
  120. package/src/codex/catalog/build-entries.ts +9 -1
  121. package/src/codex/catalog/combo-member.ts +14 -5
  122. package/src/codex/catalog/configured-tiers.ts +1 -1
  123. package/src/codex/catalog/gather-capture.ts +2 -0
  124. package/src/codex/catalog/metadata.ts +11 -9
  125. package/src/codex/catalog/model-hints.ts +21 -16
  126. package/src/codex/catalog/model-visibility.ts +3 -1
  127. package/src/codex/catalog/parsing.ts +30 -86
  128. package/src/codex/catalog/provider-models.ts +36 -34
  129. package/src/codex/catalog/routed-gather.ts +12 -13
  130. package/src/codex/cli-install-provenance.ts +105 -17
  131. package/src/codex/cli-update-plan.ts +545 -0
  132. package/src/codex/config-write-lock.ts +23 -2
  133. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  134. package/src/codex/desktop-app-restart.ts +21 -0
  135. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  136. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  137. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  138. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  139. package/src/codex/features.ts +85 -45
  140. package/src/codex/inject/history-failure.ts +27 -0
  141. package/src/codex/inject/restore.ts +24 -2
  142. package/src/codex/inject/routing-classify.ts +4 -3
  143. package/src/codex/inject.ts +18 -27
  144. package/src/codex/main-account-hard-lock.ts +30 -1
  145. package/src/codex/native-profile-startup.ts +1 -1
  146. package/src/codex/ocx-compaction-history.ts +12 -10
  147. package/src/codex/paths.ts +86 -1
  148. package/src/codex/provider-routing.ts +174 -0
  149. package/src/codex/refresh.ts +2 -2
  150. package/src/codex/routing/health-store.ts +1 -1
  151. package/src/codex/routing/selection.ts +4 -3
  152. package/src/codex/routing/thread-affinity.ts +1 -1
  153. package/src/codex/signed-in-identity.ts +43 -0
  154. package/src/codex/subagent-model-fallback.ts +1 -1
  155. package/src/codex/sync.ts +6 -1
  156. package/src/codex/user-identity.ts +16 -5
  157. package/src/codex/windows-local-appdata.ts +84 -0
  158. package/src/combos/failover.ts +6 -1
  159. package/src/combos/index.ts +1 -0
  160. package/src/combos/resolve.ts +46 -10
  161. package/src/combos/types.ts +15 -0
  162. package/src/config/diagnostics.ts +33 -6
  163. package/src/config/live-reconcile.ts +38 -2
  164. package/src/config/load-degrade.ts +17 -0
  165. package/src/config/opencode-wire-upgrade.ts +54 -0
  166. package/src/config/opencode-zen-upgrade.ts +52 -0
  167. package/src/config/owner-registry.ts +22 -17
  168. package/src/config/provider-validation.ts +45 -2
  169. package/src/config/schema/config-schema.ts +22 -0
  170. package/src/config/schema/leaf-validators.ts +38 -9
  171. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  172. package/src/config/schema/native-reasoning-retention.ts +26 -0
  173. package/src/config.ts +2 -0
  174. package/src/generated/compatibility-version.json +608 -284
  175. package/src/grok/sync.ts +1 -1
  176. package/src/images/loop.ts +46 -8
  177. package/src/integrations/catalog-refresh.ts +1 -1
  178. package/src/integrations/mutation-plan.ts +1 -0
  179. package/src/integrations/registry.ts +8 -0
  180. package/src/lib/codex-restart-contract.ts +15 -0
  181. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  182. package/src/lib/empty-tool-catalog.ts +62 -0
  183. package/src/lib/errors.ts +19 -3
  184. package/src/lib/gcp-adc.ts +96 -9
  185. package/src/lib/gcp-credential-input.ts +12 -0
  186. package/src/lib/lab-live-route-production.ts +5 -0
  187. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  188. package/src/lib/redact.ts +2 -2
  189. package/src/oauth/account-quota-rank.ts +69 -2
  190. package/src/oauth/anthropic-account-refusal.ts +20 -2
  191. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  192. package/src/oauth/anthropic-routing.ts +1 -21
  193. package/src/oauth/anthropic.ts +38 -7
  194. package/src/oauth/callback-server.ts +27 -1
  195. package/src/oauth/chatgpt-device.ts +1 -1
  196. package/src/oauth/chatgpt-shared.ts +83 -0
  197. package/src/oauth/chatgpt.ts +8 -72
  198. package/src/oauth/dsh.ts +203 -0
  199. package/src/oauth/generic-account-failover.ts +33 -7
  200. package/src/oauth/generic-session-affinity.ts +125 -0
  201. package/src/oauth/index.ts +184 -109
  202. package/src/oauth/local-token-detect.ts +21 -1
  203. package/src/oauth/login-cli-errors.ts +44 -0
  204. package/src/oauth/login-cli.ts +69 -7
  205. package/src/oauth/login-flow-state.ts +371 -23
  206. package/src/oauth/meta-muse-errors.ts +41 -0
  207. package/src/oauth/meta-muse-keychain.ts +52 -0
  208. package/src/oauth/meta-muse.ts +51 -84
  209. package/src/oauth/model-discovery-auth.ts +3 -0
  210. package/src/oauth/pool-settings-capability.ts +4 -2
  211. package/src/oauth/refresh-flight-retirement.ts +48 -0
  212. package/src/oauth/session-affinity-key.ts +23 -0
  213. package/src/oauth/store-anthropic-instance.ts +9 -2
  214. package/src/oauth/store.ts +12 -5
  215. package/src/protocols/encoders/adapter-events.ts +5 -4
  216. package/src/protocols/encoders/messages.ts +9 -6
  217. package/src/protocols/provider-summary.ts +2 -1
  218. package/src/providers/api-key-cooldowns.ts +49 -0
  219. package/src/providers/api-key-resolve.ts +11 -6
  220. package/src/providers/api-key-response-ownership.ts +57 -0
  221. package/src/providers/api-key-selection.ts +31 -6
  222. package/src/providers/api-keys.ts +214 -3
  223. package/src/providers/claude-cli-usage.ts +96 -0
  224. package/src/providers/dsh-account-destination.ts +31 -0
  225. package/src/providers/dsh-account-models.ts +41 -0
  226. package/src/providers/gcp-credential-cleanup.ts +59 -0
  227. package/src/providers/key-failover.ts +54 -50
  228. package/src/providers/key-store.ts +7 -2
  229. package/src/providers/model-rename-fields.ts +3 -0
  230. package/src/providers/model-rename-migration.ts +2 -2
  231. package/src/providers/openai-tiers-destination.ts +11 -2
  232. package/src/providers/openai-tiers.ts +5 -12
  233. package/src/providers/opencode-go-transport.ts +3 -1
  234. package/src/providers/quota/account-cache.ts +6 -5
  235. package/src/providers/quota/account-row.ts +34 -0
  236. package/src/providers/quota/antigravity.ts +28 -7
  237. package/src/providers/quota/dsh-account.ts +70 -0
  238. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  239. package/src/providers/quota-types.ts +2 -0
  240. package/src/providers/quota.ts +14 -20
  241. package/src/providers/registry/entries-core.ts +37 -8
  242. package/src/providers/registry/types.ts +1 -1
  243. package/src/providers/request-pacing.ts +77 -19
  244. package/src/providers/routed-config.ts +339 -0
  245. package/src/providers/service-tier.ts +2 -1
  246. package/src/quota/window-mapping.ts +5 -2
  247. package/src/responses/anthropic-progress.ts +34 -0
  248. package/src/responses/assistant-replay.ts +35 -0
  249. package/src/responses/compaction-history-framing.ts +65 -0
  250. package/src/responses/compaction.ts +6 -1
  251. package/src/responses/incomplete-reason.ts +31 -0
  252. package/src/responses/parser.ts +50 -5
  253. package/src/responses/reasoning-envelope.ts +4 -0
  254. package/src/responses/reasoning-replay-cache.ts +24 -2
  255. package/src/responses/reasoning-retention.ts +173 -0
  256. package/src/responses/retained-compaction.ts +46 -0
  257. package/src/responses/schema.ts +23 -0
  258. package/src/router.ts +8 -321
  259. package/src/routing/health.ts +37 -22
  260. package/src/routing/history/indexer.ts +6 -5
  261. package/src/routing/history/ingest.ts +6 -2
  262. package/src/routing/profile.ts +1 -1
  263. package/src/server/admission-model-scope.ts +17 -0
  264. package/src/server/auth-cors.ts +11 -0
  265. package/src/server/chat-completions.ts +7 -1
  266. package/src/server/chat-native-reasoning.ts +123 -0
  267. package/src/server/chat-native-sse.ts +110 -7
  268. package/src/server/chat-native.ts +76 -17
  269. package/src/server/claude-messages.ts +55 -414
  270. package/src/server/effort-policy.ts +1 -1
  271. package/src/server/images.ts +15 -2
  272. package/src/server/index/model-alias-id.ts +36 -0
  273. package/src/server/index/optional-listeners.ts +2 -0
  274. package/src/server/index/serve-options.ts +9 -1
  275. package/src/server/index/startup-warnings.ts +2 -0
  276. package/src/server/index.ts +1 -1
  277. package/src/server/inference/advisor-diagnostics.ts +31 -0
  278. package/src/server/management/advisor-routes.ts +15 -4
  279. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  280. package/src/server/management/combo-routes.ts +176 -50
  281. package/src/server/management/companion-routes.ts +1 -1
  282. package/src/server/management/config-routes.ts +75 -4
  283. package/src/server/management/cursor-integration-routes.ts +1 -1
  284. package/src/server/management/model-routes.ts +11 -6
  285. package/src/server/management/model-rows.ts +14 -0
  286. package/src/server/management/native-integration-routes.ts +2 -7
  287. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  288. package/src/server/management/oauth-account-routes.ts +87 -14
  289. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  290. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  291. package/src/server/management/provider-overwrite-carry.ts +7 -0
  292. package/src/server/management/provider-routes.ts +25 -30
  293. package/src/server/management/route-registry.ts +10 -1
  294. package/src/server/management-api.ts +3 -1
  295. package/src/server/messages-native-tool-guard.ts +185 -0
  296. package/src/server/messages-native.ts +101 -21
  297. package/src/server/messages-shared.ts +431 -0
  298. package/src/server/relay.ts +1 -1
  299. package/src/server/request-log-terminal-status.ts +1 -1
  300. package/src/server/request-log.ts +6 -1
  301. package/src/server/responses/account-change-state.ts +8 -2
  302. package/src/server/responses/adapter-continuation.ts +13 -1
  303. package/src/server/responses/adapter-delivery.ts +19 -6
  304. package/src/server/responses/adapter-dispatch.ts +19 -2
  305. package/src/server/responses/advisor-retention.ts +117 -0
  306. package/src/server/responses/advisor-slot.ts +73 -41
  307. package/src/server/responses/codex-ws-wire.ts +15 -8
  308. package/src/server/responses/compact.ts +303 -125
  309. package/src/server/responses/compaction-recovery.ts +18 -10
  310. package/src/server/responses/compaction-retention.ts +61 -0
  311. package/src/server/responses/context-overflow.ts +11 -6
  312. package/src/server/responses/core-auth.ts +6 -0
  313. package/src/server/responses/core-codex-account.ts +11 -1
  314. package/src/server/responses/core-combo-failure.ts +2 -0
  315. package/src/server/responses/core-combo.ts +120 -36
  316. package/src/server/responses/core-errors.ts +3 -3
  317. package/src/server/responses/core-lifetime.ts +5 -2
  318. package/src/server/responses/core-opaque-recovery.ts +35 -0
  319. package/src/server/responses/core-options.ts +5 -0
  320. package/src/server/responses/core-pipeline.ts +141 -0
  321. package/src/server/responses/core-replay.ts +18 -2
  322. package/src/server/responses/core.ts +2 -115
  323. package/src/server/responses/fetch-helpers.ts +73 -19
  324. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  325. package/src/server/responses/native-response-control.ts +4 -2
  326. package/src/server/responses/passthrough-delivery.ts +3 -1
  327. package/src/server/responses/passthrough-dispatch.ts +15 -13
  328. package/src/server/responses/request-prepare.ts +76 -12
  329. package/src/server/responses/request-sidecar-auth.ts +2 -2
  330. package/src/server/responses/request-transport.ts +104 -27
  331. package/src/server/responses/response-effects.ts +1 -0
  332. package/src/server/responses/run-turn-execution.ts +96 -30
  333. package/src/server/responses/sidecar-execution.ts +20 -2
  334. package/src/server/responses/terminal-guard.ts +2 -16
  335. package/src/service/auth-preflight.ts +71 -0
  336. package/src/service/cli.ts +4 -2
  337. package/src/service/definition.ts +97 -0
  338. package/src/service/guards.ts +1 -61
  339. package/src/service/health-sentinel-linux.ts +112 -0
  340. package/src/service/health-sentinel-main.ts +76 -57
  341. package/src/service/health.ts +5 -89
  342. package/src/service/launchd.ts +27 -7
  343. package/src/service/logging.ts +72 -9
  344. package/src/service/repair.ts +2 -1
  345. package/src/service/state.ts +7 -8
  346. package/src/service/systemd.ts +1 -1
  347. package/src/service/windows-guarded-manager.ts +2 -1
  348. package/src/service/windows-ops.ts +1 -1
  349. package/src/service/windows-scheduler.ts +2 -7
  350. package/src/service/windows-taskxml.ts +2 -2
  351. package/src/service/windows-wscript.ts +14 -0
  352. package/src/service.ts +4 -2
  353. package/src/sidecar/auth.ts +2 -0
  354. package/src/types/config.ts +64 -3
  355. package/src/types/opencode-go.ts +61 -0
  356. package/src/types/opencode-zen.ts +29 -0
  357. package/src/types/provider.ts +32 -6
  358. package/src/types/request.ts +23 -2
  359. package/src/types/wire.ts +29 -37
  360. package/src/types.ts +1 -0
  361. package/src/update/npm-invocation.mjs +12 -2
  362. package/src/usage/cost.ts +48 -12
  363. package/src/usage/expected-prices.ts +3 -0
  364. package/src/usage/ledger-retention.ts +2 -1
  365. package/src/usage/ledger-scanner.ts +6 -5
  366. package/src/usage/log.ts +17 -8
  367. package/src/usage/summary.ts +22 -9
  368. package/src/usage/user-cost-overlays.ts +14 -19
  369. package/src/usage/user-cost-validation.ts +38 -0
  370. package/src/vision/eligibility.ts +13 -0
  371. package/src/web-search/loop.ts +55 -5
  372. package/src/web-search/progress-stream.ts +4 -1
  373. package/src/web-search/run-turn-loop.ts +9 -1
  374. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  375. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  376. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  377. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  378. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
package/src/codex/sync.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { approvedCodexProviders } from "./provider-routing";
1
2
  import { currentExternalCodexModelProvider, injectCodexConfig } from "./inject";
2
3
  import { FOREIGN_CODEX_HOME_OWNER_MESSAGE, UNKNOWN_CODEX_HOME_OWNER_MESSAGE, unbackedRoutedRemovalMessage } from "./catalog/routed-removal";
3
4
  import { printProjectCodexConfigWarnings, groupProjectCodexConfigWarningsByPath, type ProjectCodexConfigWarning } from "./project-config-warnings";
@@ -33,6 +34,8 @@ export interface CodexSyncResult {
33
34
  catalogPath: string | null;
34
35
  catalogExists: boolean;
35
36
  catalogWritten: boolean;
37
+ /** Committed configuration-byte change, never inferred from a successful no-op. */
38
+ configChanged?: boolean;
36
39
  cacheSynced: boolean;
37
40
  /**
38
41
  * Whether the catalog owner committed a validated catalog or refused the
@@ -188,7 +191,7 @@ export async function syncModelsToCodex(
188
191
  };
189
192
  }
190
193
 
191
- if (externalProvider) {
194
+ if (externalProvider && !approvedCodexProviders().includes(externalProvider)) {
192
195
  if (catalogEvenWhenNotInjected) {
193
196
  // External providers own config.toml, and the injector removes the OpenCodex
194
197
  // journal for external providers (inject.ts). This explicit catalog-only sync
@@ -214,6 +217,7 @@ export async function syncModelsToCodex(
214
217
  return {
215
218
  status: "applied",
216
219
  ok: result.success,
220
+ ...(result.success && result.configChanged === true ? { configChanged: true } : {}),
217
221
  added: 0,
218
222
  catalogPath: null,
219
223
  catalogExists: false,
@@ -332,6 +336,7 @@ export async function syncModelsToCodex(
332
336
  return {
333
337
  status: "applied",
334
338
  ok: result.success,
339
+ ...(result.success && result.configChanged === true ? { configChanged: true } : {}),
335
340
  added,
336
341
  ...(refreshOutcome ? { refreshOutcome } : {}),
337
342
  catalogPath,
@@ -19,6 +19,7 @@ import { isAbsolute, join, resolve } from "node:path";
19
19
 
20
20
  import { resolveTrustedWindowsPowerShellExe } from "../lib/windows-elevation";
21
21
  import { WINDOWS_PRINCIPAL_LOOKUP_TIMEOUT_MS } from "../lib/windows-user-principal";
22
+ import { resolveWindowsLocalAppData } from "./windows-local-appdata";
22
23
 
23
24
  import type {
24
25
  ResolveCodexCoordinatorDatabasePath,
@@ -209,10 +210,11 @@ export function decodeWindowsIdentityPowerShellOutputForTests(output: Uint8Array
209
210
  /**
210
211
  * Per-process memo for the Windows lookups.
211
212
  *
212
- * Both values -- the effective token's SID and its known-folder local AppData --
213
+ * Both helper values -- the effective token's SID and its known-folder local AppData --
213
214
  * are fixed for the lifetime of a process: neither can change without a new logon
214
215
  * token, and the lookups deliberately ignore the environment, so nothing a caller
215
- * does between two calls can alter the answer. Each call otherwise spawns a fresh
216
+ * does between two calls can alter the answer. The native x64 known-folder path
217
+ * avoids this helper entirely. Each fallback call otherwise spawns a fresh
216
218
  * PowerShell, roughly 150ms for the SID and 310ms for the folder, and the
217
219
  * coordinator asks for both on every config write and lock acquisition. That cost
218
220
  * pushed real multi-process injection tests past their budget while proving
@@ -265,6 +267,15 @@ function resolveWindowsSid(): string {
265
267
  return sid.toUpperCase();
266
268
  }
267
269
 
270
+ function windowsLocalAppData(): string {
271
+ try {
272
+ return resolveWindowsLocalAppData(() => powershellValue(windowsLocalAppDataExpression()));
273
+ } catch (cause) {
274
+ if (cause instanceof CodexUserIdentityRefusal) throw cause;
275
+ refuse("Windows LocalAppData resolution failed.", cause);
276
+ }
277
+ }
278
+
268
279
  export const resolveEffectiveUserIdentity: ResolveEffectiveUserIdentity = () => {
269
280
  if (process.platform === "win32") {
270
281
  return { platform: "win32", sid: resolveWindowsSid() };
@@ -374,7 +385,7 @@ export function probeCodexCoordinatorNamespace(identity: UserIdentity): Coordina
374
385
  }
375
386
 
376
387
  if (!SID_PATTERN.test(identity.sid)) refuse("The coordinator identity contains an invalid SID.");
377
- const localAppData = powershellValue(windowsLocalAppDataExpression());
388
+ const localAppData = windowsLocalAppData();
378
389
  if (!isAbsolute(localAppData)) refuse("Windows LocalAppData resolution returned a relative path.");
379
390
  const root = resolve(localAppData, "OpenCodex", "Runtime", "v1", identity.sid.toUpperCase());
380
391
  let entry;
@@ -404,10 +415,10 @@ export function probeCodexCoordinatorNamespace(identity: UserIdentity): Coordina
404
415
 
405
416
  function resolveWindowsRuntimeRoot(identity: Extract<UserIdentity, { platform: "win32" }>): string {
406
417
  if (!SID_PATTERN.test(identity.sid)) refuse("The coordinator identity contains an invalid SID.");
407
- const localAppData = powershellValue(windowsLocalAppDataExpression());
418
+ const localAppData = windowsLocalAppData();
408
419
  if (!isAbsolute(localAppData)) refuse("Windows LocalAppData resolution returned a relative path.");
409
420
 
410
- // The SID and known-folder values come from the effective token/.NET OS APIs,
421
+ // The SID and known-folder values come from effective-token OS APIs,
411
422
  // never USERPROFILE or LOCALAPPDATA. WP11 adds descriptor/reparse/ACL checks at
412
423
  // the stable-database open boundary where those checks can cover SQLite too.
413
424
  const root = resolve(localAppData, "OpenCodex", "Runtime", "v1", identity.sid.toUpperCase());
@@ -0,0 +1,84 @@
1
+ import { dlopen, ptr, read, type Pointer } from "bun:ffi";
2
+ import { win32 } from "node:path";
3
+ import { resolveTrustedWindowsSystemDirectory } from "../lib/windows-elevation";
4
+
5
+ // FOLDERID_LocalAppData: GUID fields Data1/Data2/Data3 are little-endian.
6
+ const LOCAL_APPDATA_ID = Buffer.from("8527b3f1ba6fcf4f9d557b8e7f157091", "hex");
7
+ const KF_FLAG_DEFAULT_PATH = 0x400;
8
+ const MAX_PATH_CODE_UNITS = 32_768;
9
+
10
+ export interface WindowsKnownFolderApi {
11
+ getPath(id: Uint8Array, flags: number, token: null, output: BigUint64Array): number;
12
+ readCodeUnit(address: number, byteOffset: number): number;
13
+ free(address: number): void;
14
+ }
15
+
16
+ /** Same current-token/default-path API as the PowerShell fallback, without Add-Type. */
17
+ export function readWindowsLocalAppData(api: WindowsKnownFolderApi): string {
18
+ const output = new BigUint64Array(1);
19
+ let address = 0;
20
+ try {
21
+ const hr = api.getPath(LOCAL_APPDATA_ID, KF_FLAG_DEFAULT_PATH, null, output);
22
+ address = Number(output[0]);
23
+ if (hr !== 0 || !Number.isSafeInteger(address) || address <= 0) {
24
+ throw new Error("Windows LocalAppData known-folder lookup failed.");
25
+ }
26
+ const units: number[] = [];
27
+ for (let index = 0; index < MAX_PATH_CODE_UNITS; index += 1) {
28
+ const unit = api.readCodeUnit(address, index * 2);
29
+ if (unit === 0) {
30
+ const path = String.fromCharCode(...units);
31
+ if (!win32.isAbsolute(path) || !/^[a-z]:\\|^\\\\/i.test(path)) {
32
+ throw new Error("Windows LocalAppData known-folder lookup returned an invalid path.");
33
+ }
34
+ return path;
35
+ }
36
+ units.push(unit);
37
+ }
38
+ throw new Error("Windows LocalAppData known-folder lookup returned an unterminated path.");
39
+ } finally {
40
+ // SHGetKnownFolderPath requires CoTaskMemFree even on a failed HRESULT.
41
+ if (address > 0 && Number.isSafeInteger(address)) api.free(address);
42
+ }
43
+ }
44
+
45
+ let nativeLookup: (() => string) | null | undefined;
46
+
47
+ function loadNativeLookup(): (() => string) | null {
48
+ if (nativeLookup !== undefined) return nativeLookup;
49
+ if (process.platform !== "win32" || process.arch !== "x64") return null;
50
+ try {
51
+ const systemDirectory = resolveTrustedWindowsSystemDirectory();
52
+ const shell = dlopen(win32.join(systemDirectory, "shell32.dll"), {
53
+ SHGetKnownFolderPath: { args: ["ptr", "u32", "ptr", "ptr"], returns: "i32" },
54
+ });
55
+ let ole;
56
+ try {
57
+ ole = dlopen(win32.join(systemDirectory, "ole32.dll"), {
58
+ CoTaskMemFree: { args: ["ptr"], returns: "void" },
59
+ });
60
+ } catch (error) {
61
+ shell.close();
62
+ throw error;
63
+ }
64
+ const api: WindowsKnownFolderApi = {
65
+ getPath: (id, flags, token, output) => shell.symbols.SHGetKnownFolderPath(
66
+ ptr(id), flags, token, ptr(output),
67
+ ),
68
+ readCodeUnit: (address, offset) => read.u16(address as Pointer, offset),
69
+ free: address => { ole.symbols.CoTaskMemFree(address as Pointer); },
70
+ };
71
+ nativeLookup = () => readWindowsLocalAppData(api);
72
+ } catch {
73
+ nativeLookup = null;
74
+ }
75
+ return nativeLookup;
76
+ }
77
+
78
+ /** Unsupported FFI keeps the bounded trusted helper; an OS API failure still refuses. */
79
+ export function resolveWindowsLocalAppData(
80
+ powershellLookup: () => string,
81
+ lookup: (() => string) | null = loadNativeLookup(),
82
+ ): string {
83
+ return lookup ? lookup() : powershellLookup();
84
+ }
@@ -403,6 +403,9 @@ const REQUEST_SHAPE_FAILURE_CODES = new Set([
403
403
  "tool_catalog_too_large",
404
404
  "cursor_root_envelope_limit",
405
405
  "target_incompatible",
406
+ // A route that cannot read the request's media (image input to a text-only CLI route) is
407
+ // healthy for every other request, so cooling it would only block unrelated turns.
408
+ "unsupported_input_modality",
406
409
  ]);
407
410
 
408
411
  /** Credential/billing failures that every target sharing the provider inherits. */
@@ -836,7 +839,9 @@ export function comboFailureDecision(
836
839
  // says nothing about the next combo target. Structured code only, plus the explicit prose
837
840
  // form upstreams emit when they carry no code, so an unrelated 400 stays terminal.
838
841
  const failureCode = normalizedFailureCode(options?.code || error.code);
839
- if (["model_not_found", "model_unavailable", "unsupported_model"].includes(failureCode)) {
842
+ // The input-modality refusal is the same kind of verdict: this route cannot read the turn's
843
+ // media, and a later target may. Its 400 would otherwise end the chain as invalid_request_error.
844
+ if (["model_not_found", "model_unavailable", "unsupported_model", "unsupported_input_modality"].includes(failureCode)) {
840
845
  return "hop";
841
846
  }
842
847
  if (modelRefusal === "refusal") return "hop";
@@ -22,6 +22,7 @@ export {
22
22
  export {
23
23
  advanceComboAfterFailure,
24
24
  clearComboSelectionState,
25
+ comboExclusionReasons,
25
26
  NoAvailableComboTargetsError,
26
27
  noteComboFailure,
27
28
  noteComboSuccess,
@@ -55,17 +55,49 @@ export class UnknownComboError extends Error {
55
55
  export class NoAvailableComboTargetsError extends Error {
56
56
  readonly code = "combo_unavailable";
57
57
 
58
- constructor(readonly comboId: string) {
59
- super(`No available targets for combo: ${comboId}`);
58
+ constructor(readonly comboId: string, readonly reasons = "") {
59
+ super(`No available targets for combo: ${comboId}${reasons ? ` (${reasons})` : ""}`);
60
60
  this.name = "NoAvailableComboTargetsError";
61
61
  }
62
62
  }
63
63
 
64
+ function providerExclusionReason(config: OcxConfig, target: OcxComboTarget, now: number): string | undefined {
65
+ const provider = Object.hasOwn(config.providers, target.provider) ? config.providers[target.provider] : undefined;
66
+ if (!provider) return "provider missing";
67
+ if (provider.disabled === true) return "provider disabled";
68
+ return cachedProviderQuotaIsExhausted(getCachedProviderRoutingQuota(target.provider, provider, now), now, target.model)
69
+ ? "quota exhausted"
70
+ : undefined;
71
+ }
72
+
64
73
  function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget, now: number): boolean {
65
- if (!Object.hasOwn(config.providers, target.provider)) return false;
66
- const provider = config.providers[target.provider];
67
- if (!provider || provider.disabled === true) return false;
68
- return !cachedProviderQuotaIsExhausted(getCachedProviderRoutingQuota(target.provider, provider, now), now, target.model);
74
+ return providerExclusionReason(config, target, now) === undefined;
75
+ }
76
+
77
+ /**
78
+ * Why each target of a combo cannot be picked right now, for a zero-attempt refusal. Only the
79
+ * configured provider/model and a coarse label are named, never account or request data.
80
+ */
81
+ export function comboExclusionReasons(
82
+ config: OcxConfig,
83
+ comboId: string,
84
+ eligible?: (target: NormalizedComboTarget) => boolean,
85
+ now = Date.now(),
86
+ ): string {
87
+ return (getCombo(config, comboId)?.targets ?? []).flatMap(target => {
88
+ let reason = providerExclusionReason(config, target, now);
89
+ const cooldown = reason ? undefined : earliestComboCooldown(comboId, [target], now);
90
+ if (cooldown) reason = `cooldown ${Math.max(1, Math.ceil((cooldown.expiry - now) / 1000))}s`;
91
+ if (!reason && eligible) {
92
+ try {
93
+ if (!eligible(target)) reason = "ineligible for this request";
94
+ } catch {
95
+ // An eligibility check can exhaust the translator budget; the target still was not picked.
96
+ reason = "ineligible for this request";
97
+ }
98
+ }
99
+ return reason ? [`${targetKey(target)}: ${reason}`] : [];
100
+ }).join("; ");
69
101
  }
70
102
 
71
103
  /** Capture usable providers' quota cooldowns; request eligibility is deferred to exhaustion. */
@@ -100,6 +132,9 @@ function quotaWindowExhausted(percent: number | undefined, resetAt: number | und
100
132
  const MODEL_FAMILY_WINDOW_LABELS = new Set(["fable", "opus", "sonnet"]);
101
133
 
102
134
  function customWindowAppliesToModel(window: ProviderQuotaWindow, model: string | undefined): boolean {
135
+ // Exact Antigravity model readings are diagnostic only. They are not permission or
136
+ // provider-wide exhaustion evidence until a separate eligibility contract consumes them.
137
+ if (window.modelId !== undefined) return false;
103
138
  if (window.scope !== "model" || model === undefined) return true;
104
139
  const family = window.label.trim().toLowerCase();
105
140
  if (!MODEL_FAMILY_WINDOW_LABELS.has(family)) return true;
@@ -408,9 +443,10 @@ export function advanceComboAfterFailure(
408
443
  // including the pool's OWN "all accounts are cooled" 429, which refuses locally before it picks
409
444
  // one. That is what keeps this from trading a blackhole for a hammer: when nothing else is
410
445
  // holding the target back, the target cooldown still does.
411
- const accountScoped = options.cooldownScope === "target"
412
- && options.status === 429
413
- && options.failedAccount !== undefined;
446
+ // A 402 credit refusal is the provider-scoped twin: the caller names an account only after the
447
+ // pool has cooled it and still holds an eligible sibling.
448
+ const accountScoped = options.failedAccount !== undefined
449
+ && (options.status === 429 ? options.cooldownScope === "target" : options.status === 402);
414
450
  // "none" records no cooldown at all: the failure described the request, not the target, so
415
451
  // the target must stay immediately selectable for the next (differently shaped) request.
416
452
  if (options.cooldownScope !== "none" && !accountScoped) {
@@ -612,6 +648,6 @@ export function tryPickComboModel(config: OcxConfig, modelId: string, preview =
612
648
  if (!comboId) return null;
613
649
  if (!getCombo(config, comboId)) throw new UnknownComboError(comboId);
614
650
  const picked = pickComboTarget(config, comboId, { preview });
615
- if (!picked) throw new NoAvailableComboTargetsError(comboId);
651
+ if (!picked) throw new NoAvailableComboTargetsError(comboId, comboExclusionReasons(config, comboId));
616
652
  return picked;
617
653
  }
@@ -39,12 +39,16 @@ export interface NormalizedComboTarget {
39
39
  weight: number;
40
40
  /** Emergency-only target, deferred under `cooldownWaitPolicy` (#5691). */
41
41
  lastResort: boolean;
42
+ /** Pay-per-use target; requires key consent or loopback-only operator consent. */
43
+ metered: boolean;
42
44
  reasoningEfforts?: OcxComboDefaultEffort[];
43
45
  /** Optional JEV decision description. */
44
46
  modelProfile?: string;
45
47
  }
46
48
 
47
49
  export interface NormalizedComboConfig {
50
+ /** Operator consent, effective only on a loopback-only proxy. */
51
+ allowMeteredTargets?: boolean;
48
52
  strategy: OcxComboStrategy;
49
53
  stickyLimit: number;
50
54
  cooldownMs?: number;
@@ -164,6 +168,9 @@ export function comboConfigIssues(
164
168
  }
165
169
 
166
170
  const body = raw as Record<string, unknown>;
171
+ if (body.allowMeteredTargets !== undefined && typeof body.allowMeteredTargets !== "boolean") {
172
+ issues.push({ path: ["allowMeteredTargets"], message: "allowMeteredTargets must be a boolean" });
173
+ }
167
174
  if (typeof body.alias === "string") {
168
175
  const alias = body.alias.toLowerCase();
169
176
  for (const [providerName, provider] of Object.entries(providers)) {
@@ -427,6 +434,12 @@ export function comboConfigIssues(
427
434
  message: `targets[${i}].lastResort must be a boolean`,
428
435
  });
429
436
  }
437
+ if (target.metered !== undefined && typeof target.metered !== "boolean") {
438
+ issues.push({
439
+ path: ["targets", i, "metered"],
440
+ message: `targets[${i}].metered must be a boolean`,
441
+ });
442
+ }
430
443
  if (target.modelProfile !== undefined
431
444
  && (typeof target.modelProfile !== "string"
432
445
  || target.modelProfile.trim().length === 0
@@ -477,6 +490,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
477
490
  : null;
478
491
  return {
479
492
  strategy: raw.strategy ?? "failover",
493
+ ...(raw.allowMeteredTargets === true ? { allowMeteredTargets: true } : {}),
480
494
  stickyLimit: raw.stickyLimit ?? 1,
481
495
  cooldownMs: raw.cooldownMs,
482
496
  waitForCooldownMs: raw.waitForCooldownMs ?? COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS,
@@ -503,6 +517,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
503
517
  ? { modelProfile: target.modelProfile.trim() }
504
518
  : {}),
505
519
  lastResort: target.lastResort === true,
520
+ metered: target.metered === true,
506
521
  })),
507
522
  };
508
523
  }
@@ -1,13 +1,16 @@
1
1
  import { readExistingSpendSalt, spendPoolAliasesError, validatePoolAliasOwners } from "../lib/spend-pool-alias-validation";
2
2
  import { isSubagentModelEntry, rawSubagentModelForce } from "./subagent-models";
3
+ import { validRemainingQuotaAccountIds } from "../codex/account-use-remaining";
3
4
  import { protocolConfigSchema } from "./schema/config-schema";
4
5
  import { createHash } from "node:crypto";
5
6
  import { lstatSync, readFileSync } from "node:fs";
6
7
  import { join } from "node:path";
7
8
  import * as z from "zod/v4";
8
9
  import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
10
+ import { nativeReasoningRetentionConfigError } from "./schema/native-reasoning-retention";
9
11
  import { desktopCompatibilityConfigError } from "./schema/desktop-compatibility";
10
12
  import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
13
+ import { mainAccountHardLockFallbackError } from "./schema/main-account-hard-lock-fallback";
11
14
  import type { OcxConfig } from "../types";
12
15
  import { anthropicAccountPoolConfigError, anthropicOAuthInstanceConfigError, anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
13
16
  import { configReasoningPinsConfigError } from "./provider-validation";
@@ -18,6 +21,7 @@ import { isMissingPathError } from "./atomic-write";
18
21
  import { getConfigDir, getConfigPath } from "./paths";
19
22
  import { getDefaultConfig } from "./proxy-env";
20
23
  import { salvageConfigCandidate } from "./salvage";
24
+ import { sanitizeOpenCodeOverridesForLoad } from "./opencode-wire-upgrade";
21
25
  import {
22
26
  sanitizeReasoningPinsForLoad,
23
27
  sanitizeRetryOn429ForLoad,
@@ -25,6 +29,7 @@ import {
25
29
  sanitizeModelCostsForLoad,
26
30
  sanitizeAutoReviewForLoad,
27
31
  degradedListenerWarnings,
32
+ degradedReasoningRetentionWarning,
28
33
  degradedCodexAccountPriorityWarnings,
29
34
  degradedCodexQuotaAutoRefreshWarning,
30
35
  normalizeApiKeyIds,
@@ -71,6 +76,7 @@ import {
71
76
  chatgptDesktopSchema,
72
77
  chatgptDesktopConfigIssue,
73
78
  compactionRoutingSchema,
79
+ reasoningRetentionSchema,
74
80
  skillsConfigSchema,
75
81
  memoryModelsSchema,
76
82
  } from "./schema/leaf-validators";
@@ -136,6 +142,8 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
136
142
  warnings.push(...degradedCodexAccountPriorityWarnings(rawParsed, normalized));
137
143
  warnings.push(...degradedListenerWarnings(rawParsed, normalized));
138
144
  if (blockedModelRedirectsError(rawParsed)) warnings.push("blockedModelRedirects ignored: expected a map of nonempty model keys to nonempty string targets");
145
+ if (nativeReasoningRetentionConfigError(rawParsed)) warnings.push("nativeReasoningRetention ignored: expected only optional boolean modelSwitch and accountSwitch fields; both remain off");
146
+ if (mainAccountHardLockFallbackError(rawParsed)) warnings.push("codexMainAccountHardLockFallback ignored: expected a nonblank model selector and an optional declared reasoningEffort");
139
147
  const quotaAutoRefreshWarning = degradedCodexQuotaAutoRefreshWarning(rawParsed, normalized);
140
148
  if (quotaAutoRefreshWarning) warnings.push(quotaAutoRefreshWarning);
141
149
  if (rawEffort !== undefined && !isClaudeSubagentEffort(rawEffort)) {
@@ -164,6 +172,8 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
164
172
  if (codexPoolWarning) warnings.push(codexPoolWarning);
165
173
  const spendWarning = malformedSpendWarning(rawParsed);
166
174
  if (spendWarning) warnings.push(spendWarning);
175
+ const retentionWarning = degradedReasoningRetentionWarning(rawParsed, normalized);
176
+ if (retentionWarning) warnings.push(retentionWarning);
167
177
  if (rawPoolAliasesError(rawParsed)) {
168
178
  warnings.push("spendPoolAliases invalid: configured pool admission is refused until the mapping is corrected");
169
179
  }
@@ -404,12 +414,15 @@ function codexAccountPrioritiesError(value: unknown): string | null {
404
414
  return null;
405
415
  }
406
416
 
407
- function codexAccountPriorityFailbackError(value: unknown): string | null {
417
+ function codexAccountSelectionPreferencesError(value: unknown): string | null {
408
418
  const raw = rawConfigRecord(value);
409
- if (!raw || !Object.hasOwn(raw, "codexAccountPriorityFailback")) return null;
410
- const enabled = raw.codexAccountPriorityFailback;
411
- if (enabled === undefined || typeof enabled === "boolean") return null;
412
- return "schema_invalid: codexAccountPriorityFailback: must be a boolean or omitted";
419
+ if (!raw) return null;
420
+ for (const key of ["codexAccountPriorityFailback", "codexPreserveSignedInAccount"]) {
421
+ if (raw[key] !== undefined && typeof raw[key] !== "boolean") {
422
+ return `schema_invalid: ${key}: must be a boolean or omitted`;
423
+ }
424
+ }
425
+ return null;
413
426
  }
414
427
 
415
428
  /**
@@ -505,6 +518,12 @@ function creditCodexAccountIdsError(value: unknown): string | null {
505
518
  return "schema_invalid: creditCodexAccountIds: must be an array of account ids or omitted";
506
519
  }
507
520
 
521
+ function remainingQuotaAccountIdsError(value: unknown): string | null {
522
+ const ids = rawConfigRecord(value)?.codexUseRemainingQuotaAccountIds;
523
+ return ids === undefined || validRemainingQuotaAccountIds(ids) ? null
524
+ : "schema_invalid: codexUseRemainingQuotaAccountIds: must be an array of Codex account ids or omitted";
525
+ }
526
+
508
527
  function oauthOpenBrowserError(value: unknown): string | null {
509
528
  const raw = rawConfigRecord(value);
510
529
  if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null;
@@ -681,6 +700,10 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
681
700
  return { ok: false, error: "schema_invalid: chatgptDesktop: requires an optional boolean appServerShim and no other fields" };
682
701
  }
683
702
  const compactionRouting = rawConfigRecord(value)?.compactionRouting;
703
+ const reasoningRetention = rawConfigRecord(value)?.reasoningRetention;
704
+ if (reasoningRetention !== undefined && !reasoningRetentionSchema.safeParse(reasoningRetention).success) {
705
+ return { ok: false, error: "schema_invalid: reasoningRetention: requires optional boolean enabled, maxContextPercent greater than 0 and at most 100 and/or positive integer maxTokens, with no other fields" };
706
+ }
684
707
  if (compactionRouting !== undefined && !compactionRoutingSchema.safeParse(compactionRouting).success) {
685
708
  return { ok: false, error: "schema_invalid: compactionRouting: requires a nonblank model, an optional valid reasoningEffort, and optional non-repeating triggers drawn from \"manual\" and \"auto\"" };
686
709
  }
@@ -695,6 +718,8 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
695
718
  const anthropicError = anthropicOAuthInstanceConfigError(value) ?? anthropicAccountPoolConfigError(value) ?? anthropicSidecarConfigError(value);
696
719
  if (anthropicError) return { ok: false, error: anthropicError };
697
720
  const boundaryError = blockedModelRedirectsError(value)
721
+ ?? nativeReasoningRetentionConfigError(value)
722
+ ?? mainAccountHardLockFallbackError(value)
698
723
  ?? compactionRecoveryConfigError(value) ?? desktopCompatibilityConfigError(value) ?? configReasoningPinsConfigError(value)
699
724
  ?? blankHostnameError(value)
700
725
  ?? (rawSubagentModelForce(value) !== undefined && !isSubagentModelEntry(rawSubagentModelForce(value)) ? "schema_invalid: claudeCode.subagentModelForce: expected a safe roster-style model id" : null)
@@ -710,7 +735,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
710
735
  ?? codexPoolError(value)
711
736
  ?? googleAntigravityStaticCatalogVersionError(value)
712
737
  ?? codexAccountPrioritiesError(value)
713
- ?? codexAccountPriorityFailbackError(value)
738
+ ?? codexAccountSelectionPreferencesError(value)
714
739
  ?? poolCredentialGroupsError(value)
715
740
  ?? codexQuotaAutoRefreshError(value)
716
741
  ?? codexAccountPickerEnabledError(value)
@@ -719,6 +744,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
719
744
  ?? oauthOpenBrowserError(value)
720
745
  ?? showCodexCreditsError(value)
721
746
  ?? creditCodexAccountIdsError(value)
747
+ ?? remainingQuotaAccountIdsError(value)
722
748
  ?? runtimeRoleError(value)
723
749
  ?? remoteGuiConfigError(value)
724
750
  ?? clientConnectionConfigError(value)
@@ -740,6 +766,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
740
766
  export function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics {
741
767
  try {
742
768
  const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
769
+ sanitizeOpenCodeOverridesForLoad(parsed);
743
770
  sanitizeReasoningPinsForLoad(parsed);
744
771
  // Same degradation as loadConfig: a hand-edited invalid retryOn429 must not trip the
745
772
  // schema and send the caller a default-config fallback (the config command could then
@@ -1,4 +1,4 @@
1
- import type { OcxConfig, OcxProviderConfig } from "../types";
1
+ import type { OcxConfig, OcxProviderConfig, ProviderCostOverlay } from "../types";
2
2
  import { configReasoningPinsConfigError } from "./provider-validation";
3
3
  import { adoptCustomModelCatalogMigration, projectCustomModelCatalogMigration } from "../codex/custom-model-catalog-migration";
4
4
  import { refreshPreservedProviderOwner } from "../usage/user-cost-overlays";
@@ -16,7 +16,7 @@ import {
16
16
  } from "./rebase-provenance";
17
17
  import { withConfigMutationLockSync, bumpGenerationForCooperatingConfigWrite } from "./mutation-lock";
18
18
  import { ConfigWritePublishedError, persistConfigUnlocked, readRawConfigJson } from "./persist-unlocked";
19
- import { configDiagnosticsFromRaw, readConfigDiagnostics } from "./diagnostics";
19
+ import { configDiagnosticsFromRaw, readConfigDiagnostics, validateConfigCandidate } from "./diagnostics";
20
20
  import { normalizePersistedClaudeCode } from "./load-degrade";
21
21
  import { reconcileApiKeys } from "./api-key-reconcile";
22
22
  import { captureLiveTopologyBaseline, LIVE_TOPOLOGY_KEYS, projectLiveTopology } from "./live-topology";
@@ -61,6 +61,35 @@ export function adoptPersistedCodexAccountSelection(
61
61
  if (baseline) clearConfigTopLevelDeletionIntent(baseline, keys);
62
62
  }
63
63
 
64
+ type ModelCostReplacement = { provider: string; modelId: string; cost: ProviderCostOverlay | null };
65
+ const modelCostReplacements = new WeakMap<OcxConfig, ModelCostReplacement>();
66
+
67
+ /** Explicit pricing PUT owns one whole row, even when its base tuple equals the baseline. */
68
+ export function withModelCostRowReplacement(
69
+ config: OcxConfig, provider: string, modelId: string, cost: ProviderCostOverlay | null, save: () => void,
70
+ ): void {
71
+ const previous = modelCostReplacements.get(config);
72
+ const replacementCost = structuredClone(cost);
73
+ if (replacementCost?.promptLengthPricing?.mode === "automatic") delete replacementCost.promptLengthPricing;
74
+ modelCostReplacements.set(config, { provider, modelId, cost: replacementCost });
75
+ try { save(); }
76
+ finally {
77
+ if (previous) modelCostReplacements.set(config, previous);
78
+ else modelCostReplacements.delete(config);
79
+ }
80
+ }
81
+
82
+ function applyModelCostRowReplacement(config: OcxConfig): void {
83
+ const replacement = modelCostReplacements.get(config);
84
+ if (!replacement) return;
85
+ if (!Object.hasOwn(config.providers, replacement.provider)) throw new Error("pricing provider was removed before persistence");
86
+ const provider = config.providers[replacement.provider]!;
87
+ const costs = Object.assign(Object.create(null) as Record<string, ProviderCostOverlay>, provider.modelCosts ?? {});
88
+ if (replacement.cost === null) delete costs[replacement.modelId];
89
+ else costs[replacement.modelId] = structuredClone(replacement.cost);
90
+ provider.modelCosts = costs;
91
+ }
92
+
64
93
  /**
65
94
  * Adopt a committed discovery decision and its merge baseline as one synchronous step.
66
95
  * Visibility writers assign and save synchronously; any future writer yielding between
@@ -483,6 +512,10 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
483
512
  if (pinError) throw new Error(pinError);
484
513
  let published = false;
485
514
  const persist = (candidate: OcxConfig): void => {
515
+ if (modelCostReplacements.has(config)) {
516
+ const validation = validateConfigCandidate(candidate);
517
+ if (!validation.ok) throw new Error(validation.error);
518
+ }
486
519
  const changed = persistConfigUnlocked(candidate);
487
520
  published = true;
488
521
  if (changed) bumpGenerationForCooperatingConfigWrite();
@@ -544,6 +577,9 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
544
577
  }
545
578
  }
546
579
  applyConfigObjectChildDeletions(config, childDeletions);
580
+ // Recursive three-way merging preserves sibling rows; explicit PUT/reset
581
+ // replaces only its model after merging, including absent policy fields.
582
+ applyModelCostRowReplacement(config);
547
583
  if (claudeCodeBaseline.has(config)) {
548
584
  if (onDisk !== undefined) {
549
585
  const baseline = claudeCodeBaseline.get(config);
@@ -3,12 +3,15 @@ import { isSubagentModelEntry, rawSubagentModelForce } from "./subagent-models";
3
3
  import { chmodSync, existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
6
+ import { nativeReasoningRetentionConfigError } from "./schema/native-reasoning-retention";
6
7
  import { desktopCompatibilityConfigError } from "./schema/desktop-compatibility";
7
8
  import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
9
+ import { mainAccountHardLockFallbackError } from "./schema/main-account-hard-lock-fallback";
8
10
  import { anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
9
11
  import {
10
12
  modelPinnedEffortsConfigError,
11
13
  pinnedReasoningEffortConfigError,
14
+ autoReviewOverrideConfigError,
12
15
  autoReviewModelOverridesConfigError,
13
16
  autoReviewModelTargetConfigError,
14
17
  modelCapabilitiesConfigError,
@@ -131,10 +134,14 @@ export function warnDegradedCompactionRouting(rawParsed: unknown, validated: Ocx
131
134
  * the ratchet only ever moves down: a per-block call there costs a line the file does not have.
132
135
  */
133
136
  export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxConfig): void {
137
+ if (nativeReasoningRetentionConfigError(rawParsed)) console.warn("⚠️ invalid nativeReasoningRetention disabled; model and account switch allowances remain off");
134
138
  setCodexWsReuseAcrossTurns(validated.codexWsReuseAcrossTurns === true);
139
+ const retentionWarning = degradedReasoningRetentionWarning(rawParsed, validated);
140
+ if (retentionWarning) console.warn(retentionWarning);
135
141
  if (compactionRecoveryConfigError(rawParsed)) console.warn("⚠️ invalid compactionRecovery disabled; the original compaction failure is preserved");
136
142
  if (desktopCompatibilityConfigError(rawParsed)) console.warn("Invalid desktopCompatibility startup preference ignored; no desktop compatibility service will start automatically.");
137
143
  if (blockedModelRedirectsError(rawParsed)) console.warn("⚠️ invalid blockedModelRedirects ignored; provider routing remains available");
144
+ if (mainAccountHardLockFallbackError(rawParsed)) console.warn("⚠️ invalid codexMainAccountHardLockFallback ignored; the main-account hard lock still refuses while blocked");
138
145
  const chatgptDesktop = chatgptDesktopConfigIssue(rawParsed);
139
146
  if (chatgptDesktop) console.warn(`⚠️ config.json ${chatgptDesktop} — the whole chatgptDesktop block is ignored, so the ChatGPT desktop integration reads as off`);
140
147
  warnDegradedStreamMode(rawParsed, validated);
@@ -147,6 +154,11 @@ export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxCon
147
154
  }
148
155
  }
149
156
 
157
+ export function degradedReasoningRetentionWarning(rawParsed: unknown, validated: OcxConfig): string | null {
158
+ if (rawConfigRecord(rawParsed)?.reasoningRetention === undefined || validated.reasoningRetention !== undefined) return null;
159
+ return "config.json reasoningRetention is invalid — reasoning retention is disabled";
160
+ }
161
+
150
162
  /**
151
163
  * A malformed `memoryModels` phase disables that phase rather than failing the whole schema, so
152
164
  * say so once: silently keeping whatever route the phase already had — which may be the shadow
@@ -411,6 +423,11 @@ export function sanitizeModelCostsForLoad(parsed: unknown): void {
411
423
  export function sanitizeAutoReviewForLoad(parsed: unknown): void {
412
424
  if (!parsed || typeof parsed !== "object") return;
413
425
  const root = parsed as Record<string, unknown>;
426
+ if (root.autoReviewOverride !== undefined
427
+ && autoReviewOverrideConfigError(root.autoReviewOverride) !== null) {
428
+ console.warn("⚠️ config.json autoReviewOverride is invalid — ignoring the override");
429
+ delete root.autoReviewOverride;
430
+ }
414
431
  const providers = root.providers;
415
432
  if (!providers || typeof providers !== "object" || Array.isArray(providers)) return;
416
433
  for (const [name, providerValue] of Object.entries(providers as Record<string, unknown>)) {