@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
@@ -0,0 +1,54 @@
1
+ import { OPENCODE_GO_LEGACY_NAME_PINS } from "../types/opencode-go";
2
+ import { captureWireAdapterHardPins } from "../types/wire";
3
+ import { declaredUnsupportedHostedTools, isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
4
+ import { sanitizeOpenCodeZenOverridesForLoad } from "./opencode-zen-upgrade";
5
+
6
+ function record(value: unknown): Record<string, unknown> | undefined {
7
+ return value !== null && typeof value === "object" && !Array.isArray(value)
8
+ ? value as Record<string, unknown> : undefined;
9
+ }
10
+
11
+ /** File reads alone repair overrides admitted before OpenCode's native-wire upgrade. */
12
+ export function sanitizeOpenCodeOverridesForLoad(parsed: unknown): void {
13
+ sanitizeOpenCodeZenOverridesForLoad(parsed);
14
+ const providers = record(record(parsed)?.providers);
15
+ const provider = providers && Object.hasOwn(providers, "opencode-go")
16
+ ? record(providers["opencode-go"]) : undefined;
17
+ if (!provider) return;
18
+ // Use validation's destination/auth rules, including the Go preset's fixed URL.
19
+ const pins = captureWireAdapterHardPins("opencode-go", provider);
20
+ const newlyPinned = (model: string): boolean => Object.hasOwn(pins, model)
21
+ && !OPENCODE_GO_LEGACY_NAME_PINS.has(model);
22
+ const overrides = record(provider.modelAdapters) ?? {};
23
+ const preferences = record(provider.modelPreferHostedTools);
24
+ let preferencesChanged = false;
25
+ if (preferences) {
26
+ const unsupported = declaredUnsupportedHostedTools(provider as { unsupportedHostedTools?: readonly string[] });
27
+ for (const [model, tools] of Object.entries(preferences)) {
28
+ // Go's registry owns the old provider-wide Chat wire. Only an explicit
29
+ // Responses override could previously make these native models eligible.
30
+ if (!newlyPinned(model.trim()) || overrides[model] !== "openai-responses"
31
+ || !Array.isArray(tools) || tools.length === 0 || tools.some(tool => tool !== "image_generation")
32
+ || isHostedToolUnsupportedForModel(model, "image_generation", undefined, unsupported)) continue;
33
+ delete preferences[model];
34
+ preferencesChanged = true;
35
+ }
36
+ if (preferencesChanged && Object.keys(preferences).length === 0) delete provider.modelPreferHostedTools;
37
+ }
38
+ let overridesChanged = false;
39
+ for (const [model, adapter] of Object.entries(overrides)) {
40
+ // Leave malformed or previously incompatible preferences visible on every pass.
41
+ if (preferences && Object.hasOwn(preferences, model)) continue;
42
+ if (newlyPinned(model.trim()) && (adapter === "openai-chat" || adapter === "openai-responses")) {
43
+ delete overrides[model];
44
+ overridesChanged = true;
45
+ }
46
+ }
47
+ if (overridesChanged) {
48
+ if (Object.keys(overrides).length === 0) delete provider.modelAdapters;
49
+ console.warn("OpenCode Go native models now use their required APIs; obsolete Chat/Responses overrides were ignored while loading config.json.");
50
+ }
51
+ if (preferencesChanged) {
52
+ console.warn("OpenCode Go native models now use their required APIs; legacy Responses-only hosted-tool preferences were ignored while loading config.json.");
53
+ }
54
+ }
@@ -0,0 +1,52 @@
1
+ import { openCodeZenBaseUrl } from "../types/opencode-zen";
2
+ import { declaredUnsupportedHostedTools, isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
3
+
4
+ function record(value: unknown): Record<string, unknown> | undefined {
5
+ return value !== null && typeof value === "object" && !Array.isArray(value)
6
+ ? value as Record<string, unknown> : undefined;
7
+ }
8
+
9
+ function isGeminiModel(model: string): boolean {
10
+ return model.trim().toLowerCase().startsWith("gemini-");
11
+ }
12
+
13
+ /** Repair legacy Zen wire overrides and dependent preferences in file-backed reads only. */
14
+ export function sanitizeOpenCodeZenOverridesForLoad(parsed: unknown): void {
15
+ const providers = record(record(parsed)?.providers);
16
+ const provider = providers && Object.hasOwn(providers, "opencode-zen")
17
+ ? record(providers["opencode-zen"]) : undefined;
18
+ if (!provider || !openCodeZenBaseUrl(provider)) return;
19
+ const overrides = record(provider.modelAdapters) ?? {};
20
+ const preferences = record(provider.modelPreferHostedTools);
21
+ let preferencesChanged = false;
22
+ if (preferences) {
23
+ const unsupported = declaredUnsupportedHostedTools(provider as { unsupportedHostedTools?: readonly string[] });
24
+ for (const [model, tools] of Object.entries(preferences)) {
25
+ // Capture the old wire before deleting overrides. An explicit non-Responses entry
26
+ // must not inherit a provider-wide Responses default for this migration.
27
+ const oldAdapter = Object.hasOwn(overrides, model) ? overrides[model] : provider.adapter;
28
+ if (!isGeminiModel(model) || oldAdapter !== "openai-responses"
29
+ || !Array.isArray(tools) || tools.length === 0 || tools.some(tool => tool !== "image_generation")
30
+ || isHostedToolUnsupportedForModel(model, "image_generation", undefined, unsupported)) continue;
31
+ delete preferences[model];
32
+ preferencesChanged = true;
33
+ }
34
+ if (preferencesChanged && Object.keys(preferences).length === 0) delete provider.modelPreferHostedTools;
35
+ }
36
+ let overridesChanged = false;
37
+ for (const [model, adapter] of Object.entries(overrides)) {
38
+ // Retain invalid preference/wire pairs so repeated normalization cannot admit them.
39
+ if (preferences && Object.hasOwn(preferences, model)) continue;
40
+ if (isGeminiModel(model) && (adapter === "openai-chat" || adapter === "openai-responses")) {
41
+ delete overrides[model];
42
+ overridesChanged = true;
43
+ }
44
+ }
45
+ if (overridesChanged) {
46
+ if (Object.keys(overrides).length === 0) delete provider.modelAdapters;
47
+ console.warn("OpenCode Zen Gemini models now use the native Google API; obsolete Chat/Responses overrides were ignored while loading config.json.");
48
+ }
49
+ if (preferencesChanged) {
50
+ console.warn("OpenCode Zen Gemini models now use the native Google API; legacy Responses-only hosted-tool preferences were ignored while loading config.json.");
51
+ }
52
+ }
@@ -24,21 +24,19 @@
24
24
  */
25
25
 
26
26
  import { createHash } from "node:crypto";
27
- import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
27
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, unlinkSync } from "node:fs";
28
28
  import { homedir } from "node:os";
29
29
  import { dirname, join, resolve } from "node:path";
30
30
  import { atomicWriteFileNoFollow } from "./atomic-write";
31
31
  import { getConfigDir } from "./paths";
32
- import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
32
+ import { assertNotRealHomeUnderTest, isTestHomeGuardArmed } from "../lib/test-home-guard";
33
33
  import { hardenSecretDir } from "../lib/windows-secret-acl";
34
34
 
35
35
  const REGISTRY_DIR_NAME = "ocx-homes";
36
36
  /**
37
- * Test seam, not a user knob: os.homedir() resolves the passwd database on POSIX,
38
- * so rewriting HOME inside a test process cannot move the anchor and the armed
39
- * test-home guard would rightly refuse the write. Suites point this at their own
40
- * fixture home; production always leaves it unset so the default home stays the
41
- * one locator every sibling can find.
37
+ * Test seam, not a user knob. The sandbox pins this for inherited child environments;
38
+ * guarded callers without it use their selected config home. Production leaves it
39
+ * unset so the default home stays the one locator every sibling can find.
42
40
  */
43
41
  const REGISTRY_DIR_ENV = "OCX_OWNER_REGISTRY_DIR";
44
42
  const REGISTRY_ENTRY_SUFFIX = ".json";
@@ -60,8 +58,23 @@ function registryBaseDir(): string {
60
58
  /** The shared directory the registry lives under. */
61
59
  export function ownerRegistryDir(): string {
62
60
  const override = process.env[REGISTRY_DIR_ENV]?.trim();
63
- if (override) return resolve(override);
64
- return join(registryBaseDir(), REGISTRY_DIR_NAME);
61
+ const dir = override ? resolve(override)
62
+ : join(isTestHomeGuardArmed() ? getConfigDir() : registryBaseDir(), REGISTRY_DIR_NAME);
63
+ // Bun caches the startup home, so HOME rewrites cannot isolate this anchor.
64
+ // Check reads and removals too, including an inherited override or symlink that
65
+ // points back into the protected home. An unpinned guarded caller fails closed.
66
+ if (isTestHomeGuardArmed()) {
67
+ for (let ancestor = dir; ;) {
68
+ assertNotRealHomeUnderTest(ancestor);
69
+ // Follow an existing alias before walking parents; missing children reach
70
+ // their nearest existing ancestor on the next iterations.
71
+ try { ancestor = realpathSync.native(ancestor); } catch { /* not created yet */ }
72
+ const parent = dirname(ancestor);
73
+ if (parent === ancestor) break;
74
+ ancestor = parent;
75
+ }
76
+ }
77
+ return dir;
65
78
  }
66
79
 
67
80
  function registryEntryPath(dir: string, home: string): string {
@@ -76,14 +89,6 @@ function registryEntryPath(dir: string, home: string): string {
76
89
  export function registerOwnerRegistryHome(home: string): void {
77
90
  try {
78
91
  const dir = ownerRegistryDir();
79
- // Guard every ancestor of the write target: an override that still resolves
80
- // under the protected ~/.opencodex must not slip past the test home guard.
81
- for (let ancestor = dir; ;) {
82
- assertNotRealHomeUnderTest(ancestor);
83
- const parent = dirname(ancestor);
84
- if (parent === ancestor) break;
85
- ancestor = parent;
86
- }
87
92
  mkdirSync(dir, { recursive: true, mode: 0o700 });
88
93
  // This locator contains no secret, but its deterministic entry names must not
89
94
  // give another local user a place to plant a destination symlink. Refuse a
@@ -1,4 +1,5 @@
1
1
  import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
2
+ import { resolveDshAccountMessagesUrl } from "../providers/dsh-account-destination";
2
3
  import { redactSecretString } from "../lib/redact";
3
4
  import {
4
5
  isValidModelDiscoveryModelId,
@@ -256,10 +257,11 @@ export function autoReviewModelTargetConfigError(
256
257
  field = "autoReviewModel",
257
258
  allowClear = false,
258
259
  ): string | null {
259
- if (value === undefined || (allowClear && (value === null || value === ""))) return null;
260
+ if (value === undefined || (allowClear && value === null)) return null;
260
261
  if (typeof value !== "string") return `${field} must be a string`;
261
262
  const trimmed = value.trim();
262
- if (!trimmed) return `${field} must be nonblank`;
263
+ // `allowClear` callers trim then delete, so whitespace-only is a clear, not a blank selector.
264
+ if (!trimmed) return allowClear ? null : `${field} must be nonblank`;
263
265
  if (trimmed.length > 1024 || AUTO_REVIEW_MODEL_CONTROL_CHARS.test(trimmed)) {
264
266
  return `${field} must be a catalog selector without whitespace or control characters`;
265
267
  }
@@ -271,6 +273,26 @@ export function isValidAutoReviewModel(value: unknown): value is string {
271
273
  return typeof value === "string" && autoReviewModelTargetConfigError(value) === null;
272
274
  }
273
275
 
276
+ /**
277
+ * Validate the global auto-review override block the Models settings panel writes.
278
+ *
279
+ * `enabled` is optional so a save may carry only the model during an edit; `model` follows the
280
+ * same selector rules as the provider-scoped field, and an empty value clears the override.
281
+ */
282
+ export function autoReviewOverrideConfigError(value: unknown, field = "autoReviewOverride"): string | null {
283
+ if (value === undefined || value === null) return null;
284
+ if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;
285
+ const prototype = Object.getPrototypeOf(value);
286
+ if (prototype !== Object.prototype && prototype !== null) {
287
+ return `${field} must be a plain object with own properties`;
288
+ }
289
+ const block = value as Record<string, unknown>;
290
+ if (block.enabled !== undefined && typeof block.enabled !== "boolean") {
291
+ return `${field}.enabled must be a boolean`;
292
+ }
293
+ return autoReviewModelTargetConfigError(block.model, `${field}.model`, true);
294
+ }
295
+
274
296
  /** Canonical model key used for map matching, duplicate detection, and route tombstones. */
275
297
  export function canonicalAutoReviewModelKey(modelId: string): string {
276
298
  return encodeRoutedModelId(modelId.trim());
@@ -390,6 +412,27 @@ export function modelAdapterRecordConfigError(
390
412
  }
391
413
 
392
414
 
415
+ /**
416
+ * The `dsh-account` row owns the DeepSeek Harness account token, which every helper resolves by this
417
+ * provider name. Keeping the row on its own adapter and DeepSeek's Anthropic host means no generic
418
+ * helper (Anthropic sidecars, discovery, probes) can be pointed at another destination with it.
419
+ */
420
+ export function credentialPinnedProviderConfigError(
421
+ providerName: string,
422
+ provider: { adapter?: unknown; baseUrl?: unknown },
423
+ ): string | null {
424
+ if (providerName !== "dsh-account") return null;
425
+ if (provider.adapter !== undefined && provider.adapter !== "dsh-account") {
426
+ return 'adapter must stay "dsh-account": its account token is pinned to that adapter';
427
+ }
428
+ if (typeof provider.baseUrl === "string") {
429
+ try { resolveDshAccountMessagesUrl(provider.baseUrl); } catch {
430
+ return "baseUrl must stay on https://api.deepseek.com/anthropic: its account token is pinned to that host";
431
+ }
432
+ }
433
+ return null;
434
+ }
435
+
393
436
  function capabilityRecord(value: unknown): value is Record<string, unknown> {
394
437
  return value !== null && typeof value === "object" && !Array.isArray(value)
395
438
  && [Object.prototype, null].includes(Object.getPrototypeOf(value));
@@ -1,9 +1,12 @@
1
1
  import { MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT } from "../../codex/quota-types";
2
+ import { validRemainingQuotaAccountIds } from "../../codex/account-use-remaining";
2
3
  import * as z from "zod/v4";
3
4
  import { anthropicAccountPoolSchema } from "./anthropic-account-pool";
4
5
  import { compactionRecoverySchema } from "./compaction-recovery";
6
+ import { nativeReasoningRetentionSchema } from "./native-reasoning-retention";
5
7
  import { desktopCompatibilitySchema } from "./desktop-compatibility";
6
8
  import { blockedModelRedirectsSchema } from "./blocked-model-redirects";
9
+ import { mainAccountHardLockFallbackSchema } from "./main-account-hard-lock-fallback";
7
10
  import {
8
11
  agentTaskRecoverySchema,
9
12
  catalogAutoRefreshSchema,
@@ -29,7 +32,9 @@ import {
29
32
  CODEX_ACCOUNT_NAMESPACE_ACCOUNT_ID_COLLISION_ERROR,
30
33
  codexAccountNamespacesSchema,
31
34
  modelPinnedEffortsSchema,
35
+ autoReviewOverrideSchema,
32
36
  compactionRoutingSchema,
37
+ reasoningRetentionSchema,
33
38
  memoryModelSettingSchema,
34
39
  memoryModelsSchema,
35
40
  modelPreferHostedToolsConfigError,
@@ -49,6 +54,7 @@ import {
49
54
  providerHeadersConfigError,
50
55
  providerForwardClientHeadersConfigError,
51
56
  reasoningSummaryDeliveryRecordConfigError,
57
+ credentialPinnedProviderConfigError,
52
58
  } from "../provider-validation";
53
59
  import {
54
60
  CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR,
@@ -190,9 +196,15 @@ export const configSchema = z.object({
190
196
  z.object({ enabled: z.literal(true), port: z.number().int().min(1).max(65535).optional() }),
191
197
  ]).optional().catch(undefined),
192
198
  providers: z.record(z.string(), providerConfigSchema),
199
+ // Models settings panel writes the global reviewer override; an invalid hand edit is
200
+ // stripped by load degradation so it cannot reject the providers that carry it.
201
+ autoReviewOverride: autoReviewOverrideSchema.optional(),
193
202
  modelPinnedEfforts: modelPinnedEffortsSchema.optional(),
194
203
  compactionRouting: compactionRoutingSchema.optional().catch(undefined),
195
204
  compactionRecovery: compactionRecoverySchema.optional().catch(undefined),
205
+ // A bad hand edit disables only this block; writes validate the catch-free leaf first.
206
+ reasoningRetention: reasoningRetentionSchema.optional().catch(undefined),
207
+ nativeReasoningRetention: nativeReasoningRetentionSchema.optional().catch(undefined),
196
208
  desktopCompatibility: desktopCompatibilitySchema.optional().catch(undefined),
197
209
  // A hand-edited malformed phase disables only that phase instead of rejecting
198
210
  // providers/apiKeys, matching the load-time degradation notice; the management write
@@ -207,6 +219,7 @@ export const configSchema = z.object({
207
219
  .catch(undefined),
208
220
  defaultProvider: z.string().min(1).default("openai"),
209
221
  defaultModelAliases: z.boolean().optional(),
222
+ preferModelAliasIds: z.boolean().optional().catch(false),
210
223
  // Malformed hand edits disable this opt-in projection without rejecting providers.
211
224
  cursorEffortRows: z.boolean().optional().catch(false),
212
225
  // Fast selectors default on; malformed hand edits disable them without rejecting providers.
@@ -221,6 +234,9 @@ export const configSchema = z.object({
221
234
  short: z.number().int().min(MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT).max(100).optional().catch(undefined),
222
235
  long: z.number().int().min(MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT).max(100).optional().catch(undefined),
223
236
  }).optional().catch(undefined),
237
+ // A malformed fallback section disables only the reroute, never the lock's refusal
238
+ // behavior; the management write boundary still rejects the bad value.
239
+ codexMainAccountHardLockFallback: mainAccountHardLockFallbackSchema.optional().catch(undefined),
224
240
  // #2999: a malformed hand edit falls back to auto detection rather than rejecting the config.
225
241
  nativeMainRefresh: z.enum(["codex", "opencodex"]).optional().catch(undefined),
226
242
  // Future versions remain opaque through passthrough-compatible whole-config saves.
@@ -292,6 +308,7 @@ export const configSchema = z.object({
292
308
  // so a hand-edited typo cannot trip the backup-and-defaults repair path; the write path rejects
293
309
  // it (creditCodexAccountIdsError in diagnostics).
294
310
  creditCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional().catch(undefined),
311
+ codexUseRemainingQuotaAccountIds: z.custom<string[]>(validRemainingQuotaAccountIds).optional().catch(undefined),
295
312
  // A malformed policy degrades to "no policy" rather than failing the parse, so a hand-edited
296
313
  // typo cannot trip the backup-and-defaults repair path and wipe providers or pool accounts.
297
314
  // Silently ignoring it would be its own trap, so the write path rejects it and loadConfig warns.
@@ -307,6 +324,7 @@ export const configSchema = z.object({
307
324
  codexAccountAutoSwitchThresholds: z.unknown().optional().transform(salvageCodexAccountAutoSwitchThresholds),
308
325
  // An invalid optional preference must not discard providers or credential rows.
309
326
  codexAccountPriorityFailback: z.boolean().optional().catch(false),
327
+ codexPreserveSignedInAccount: z.boolean().optional().catch(false),
310
328
  // Absent or invalid keeps the sliding window on. Explicit false restores consecutive-only steering.
311
329
  codexFailureWindow: z.boolean().optional().catch(undefined),
312
330
  codexPinnedTransientPolicy: z.enum(["hold", "detour-new-threads"]).optional().catch("hold"),
@@ -594,6 +612,10 @@ export const configSchema = z.object({
594
612
  message: modelAdaptersError,
595
613
  });
596
614
  }
615
+ const credentialPinError = credentialPinnedProviderConfigError(name, provider);
616
+ if (credentialPinError) {
617
+ ctx.addIssue({ code: "custom", path: ["providers", redactSecretString(name)], message: credentialPinError });
618
+ }
597
619
  const preferHostedToolsError = modelPreferHostedToolsConfigError(
598
620
  (provider as { modelPreferHostedTools?: unknown }).modelPreferHostedTools,
599
621
  "modelPreferHostedTools",
@@ -6,6 +6,7 @@ import {
6
6
  modelPinnedEffortsConfigError,
7
7
  pinnedReasoningEffortConfigError,
8
8
  modelDisplayNamesConfigError,
9
+ autoReviewOverrideConfigError,
9
10
  autoReviewModelOverridesConfigError,
10
11
  autoReviewModelTargetConfigError,
11
12
  normalizeNonBlankStringArray,
@@ -28,13 +29,14 @@ import {
28
29
  PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS,
29
30
  UPSTREAM_HTTP_VERSION_VALUES,
30
31
  type OcxProviderConfig,
32
+ type OcxConfig,
31
33
  type FastWire,
32
34
  type ProviderCostOverlay,
33
35
  } from "../../types";
34
36
  import { fastWireDeclarationError } from "../../providers/fastwire";
35
37
  import { getProviderRegistryEntry, providerMatchesRegistryTransport, providerModelWireDefault } from "../../providers/registry";
36
38
  import { resolveOpenAiVirtualModel } from "../../providers/openai-virtual-models";
37
- import { COST4_RATE_KEYS, isValidCost4Rate } from "../../usage/user-cost-overlays";
39
+ import { COST4_RATE_KEYS, isValidCost4Rate, isValidPromptLengthPricing, copyPromptLengthPricing } from "../../usage/user-cost-overlays";
38
40
  import { MAX_COST4_RATE } from "../../usage/expected-prices";
39
41
  import {
40
42
  DECLARABLE_HOSTED_TOOL_TYPES,
@@ -63,6 +65,12 @@ export const compactionRoutingSchema = z.object({
63
65
  .optional(),
64
66
  }).strict();
65
67
 
68
+ export const reasoningRetentionSchema = z.object({
69
+ enabled: z.boolean().optional(),
70
+ maxContextPercent: z.number().gt(0).max(100).optional(),
71
+ maxTokens: z.number().int().positive().optional(),
72
+ }).strict();
73
+
66
74
  /**
67
75
  * One phase of Codex's memory pipeline. A present phase must name a model: the GUI's "Off"
68
76
  * removes the phase instead of blanking it, so an empty entry would only ever come from a
@@ -133,12 +141,15 @@ const requestPacingSchema = z.object({
133
141
  minIntervalMs: z.number().int().min(1).max(3_600_000).optional(),
134
142
  maxConcurrentRequests: z.number().int().min(1).optional(),
135
143
  models: z.record(z.string().trim().min(1), requestPacingRuleSchema).optional(),
144
+ keySelection: z.enum(["active", "round-robin"]).optional(),
145
+ keys: requestPacingRuleSchema.optional(),
136
146
  }).strict().refine(value => value.enabled === false
137
147
  || value.requestsPerMinute !== undefined
138
148
  || value.minIntervalMs !== undefined
139
149
  || value.maxConcurrentRequests !== undefined
150
+ || value.keys !== undefined || value.keySelection === "round-robin"
140
151
  || (value.models !== undefined && Object.keys(value.models).length > 0), {
141
- message: "enabled request pacing needs a provider rule or model override",
152
+ message: "enabled request pacing needs a provider, model, or key rule, or round-robin selection",
142
153
  });
143
154
 
144
155
  export function requestPacingConfigError(value: unknown): string | null {
@@ -257,6 +268,12 @@ const autoReviewModelOverridesSchema = z.unknown().superRefine((value, ctx) => {
257
268
  if (error) ctx.addIssue({ code: "custom", message: error });
258
269
  }).transform(value => normalizeAutoReviewModelOverrides(value));
259
270
 
271
+ /** Global auto-review override block; a malformed hand edit is stripped by load degradation. */
272
+ export const autoReviewOverrideSchema = z.unknown().superRefine((value, ctx) => {
273
+ const error = autoReviewOverrideConfigError(value, "autoReviewOverride");
274
+ if (error) ctx.addIssue({ code: "custom", message: error });
275
+ }).transform(value => value as NonNullable<OcxConfig["autoReviewOverride"]>);
276
+
260
277
  const modelCapabilitiesSchema = z.unknown().superRefine((value, ctx) => {
261
278
  const error = modelCapabilitiesConfigError(value);
262
279
  if (error) ctx.addIssue({ code: "custom", message: error });
@@ -337,9 +354,12 @@ export const providerConfigSchema = z.object({
337
354
  preserveResponsesReasoningContent: z.boolean().optional(),
338
355
  preserveResponsesInputItemIds: z.boolean().optional(),
339
356
  preserveResponsesMessageMetadata: z.boolean().optional(),
357
+ projectResponsesLiteHostedSearchHistory: z.boolean().optional(),
340
358
  dropResponsesReasoningItems: z.boolean().optional(),
341
359
  modelReasoningEffortsAuthoritative: z.boolean().optional(),
342
360
  decodesNativeCompactionBlobs: z.boolean().optional(),
361
+ supportsNativeCompactionTrigger: z.boolean().optional(),
362
+ supportsNativeCompactEndpoint: z.boolean().optional(),
343
363
  allowEncryptedV2AgentTasks: z.boolean().optional(),
344
364
  allowPrivateNetwork: z.boolean().optional(),
345
365
  // Per-provider egress (#2894): absent inherits the global proxy decision, "direct"/null
@@ -423,7 +443,7 @@ export { providerRelativeSendPathConfigError } from "../provider-relative-send-p
423
443
 
424
444
  /**
425
445
  * Validate `providers.<name>.modelCosts`: a plain object keyed by exact model
426
- * id, each value a 4-tuple of non-negative finite USD-per-1M-token rates.
446
+ * id, each value four USD-per-1M-token rates and an optional prompt-length policy.
427
447
  * Returns null when valid/absent, else a human-readable error.
428
448
  */
429
449
  export function providerModelCostsConfigError(value: unknown, field = "modelCosts"): string | null {
@@ -443,25 +463,28 @@ export function providerModelCostsConfigError(value: unknown, field = "modelCost
443
463
  const rates = entry as Record<string, unknown>;
444
464
  for (const key of COST4_RATE_KEYS) {
445
465
  const rate = rates[key];
446
- if (!isValidCost4Rate(rate)) {
466
+ if (!Object.hasOwn(rates, key) || !isValidCost4Rate(rate)) {
447
467
  return `${field}.${safeModelId}.${key} must be a non-negative finite number at most ${MAX_COST4_RATE} (USD per 1M tokens)`;
448
468
  }
449
469
  }
470
+ if (Object.hasOwn(rates, "promptLengthPricing") && !isValidPromptLengthPricing(rates.promptLengthPricing)) {
471
+ return `${field}.${safeModelId}.promptLengthPricing must be automatic, flat, or a complete custom policy with a positive safe-integer thresholdTokens, gt/gte comparison, and four valid rates`;
472
+ }
450
473
  // Reject unknown fields: a misplaced apiKey/apiKeyPool under a cost row
451
474
  // would otherwise be persisted and echoed verbatim by display paths that
452
475
  // mask only top-level provider secrets.
453
476
  const extraKeys = Object.keys(rates)
454
- .filter((key) => !(COST4_RATE_KEYS as readonly string[]).includes(key));
477
+ .filter((key) => key !== "promptLengthPricing" && !(COST4_RATE_KEYS as readonly string[]).includes(key));
455
478
  if (extraKeys.length > 0) {
456
- return `${field}.${safeModelId} has unexpected fields ${JSON.stringify(extraKeys.map(redactSecretString).join(", "))} — only input, output, cacheRead, and cacheWrite are allowed (USD per 1M tokens)`;
479
+ return `${field}.${safeModelId} has unexpected fields ${JSON.stringify(extraKeys.map(redactSecretString).join(", "))} — only input, output, cacheRead, cacheWrite, and promptLengthPricing are allowed (USD per 1M tokens)`;
457
480
  }
458
481
  }
459
482
  return null;
460
483
  }
461
484
 
462
485
  /**
463
- * Serialize `providers.<name>.modelCosts` for display: copy ONLY the four
464
- * numeric rate fields per model and DROP secret-shaped model ids, so a pasted
486
+ * Serialize `providers.<name>.modelCosts` for display: copy the four validated
487
+ * rates and complete validated policy and DROP secret-shaped model ids, so a pasted
465
488
  * API key in a key position cannot be echoed back by CLI/DTO display paths.
466
489
  * The result uses a null prototype so "__proto__" remains an own row.
467
490
  */
@@ -471,6 +494,7 @@ export function sanitizeModelCostsForDisplay(costs: unknown): Record<string, Pro
471
494
  for (const [modelId, entry] of Object.entries(costs)) {
472
495
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
473
496
  const rates = entry as Record<string, unknown>;
497
+ if (Object.hasOwn(rates, "promptLengthPricing") && !isValidPromptLengthPricing(rates.promptLengthPricing)) continue;
474
498
  const input = rates.input;
475
499
  const output = rates.output;
476
500
  const cacheRead = rates.cacheRead;
@@ -484,7 +508,10 @@ export function sanitizeModelCostsForDisplay(costs: unknown): Record<string, Pro
484
508
  // Secret-shaped ids are DROPPED rather than mapped to "[REDACTED]" so
485
509
  // distinct rows cannot collapse into one placeholder key.
486
510
  if (redactSecretString(modelId) !== modelId) continue;
487
- out[modelId] = { input, output, cacheRead, cacheWrite };
511
+ const policy = isValidPromptLengthPricing(rates.promptLengthPricing)
512
+ ? copyPromptLengthPricing(rates.promptLengthPricing) : undefined;
513
+ out[modelId] = { input, output, cacheRead, cacheWrite,
514
+ ...(policy ? { promptLengthPricing: policy } : {}) };
488
515
  }
489
516
  }
490
517
  return Object.keys(out).length > 0 ? out : undefined;
@@ -794,6 +821,8 @@ export const apiKeyEntrySchema = z.object({
794
821
  // rather than promoting it.
795
822
  allowedProviders: z.array(z.string().trim().min(1).max(256)).optional(),
796
823
  allowedModels: z.array(z.string().trim().min(1).max(256)).optional(),
824
+ // Not `.catch`ed for the same reason: a damaged value must not grant metered spend.
825
+ allowMeteredComboTargets: z.boolean().optional(),
797
826
  }).passthrough();
798
827
 
799
828
  /**
@@ -0,0 +1,26 @@
1
+ import * as z from "zod/v4";
2
+ import { pinnedReasoningEffortConfigError } from "../provider-validation";
3
+
4
+ /**
5
+ * Extended settings of the main-account hard lock (`codexMainAccountHardLockFallback`):
6
+ * while the lock blocks, a qualifying Codex-native request reroutes to `model` instead of
7
+ * failing. The dashboard row keeps the configured model when the switch is off, so
8
+ * `enabled: false` is a remembered pause, not a cleared setting; an absent section, an
9
+ * absent model, and a malformed section all mean the same thing — the lock refuses,
10
+ * exactly as it does today.
11
+ */
12
+ export const mainAccountHardLockFallbackSchema = z.object({
13
+ enabled: z.boolean().optional(),
14
+ model: z.string().refine(model => model.length > 0 && model === model.trim() && !/\s/.test(model)),
15
+ reasoningEffort: z.string().refine(value => pinnedReasoningEffortConfigError(value) === null).optional(),
16
+ }).strict();
17
+
18
+ /** Candidate writes must reject sections the read schema would silently discard. */
19
+ export function mainAccountHardLockFallbackError(value: unknown): string | undefined {
20
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
21
+ const raw = (value as Record<string, unknown>).codexMainAccountHardLockFallback;
22
+ if (raw === undefined) return undefined;
23
+ return mainAccountHardLockFallbackSchema.safeParse(raw).success
24
+ ? undefined
25
+ : "schema_invalid: codexMainAccountHardLockFallback requires a nonblank model selector and an optional declared reasoningEffort";
26
+ }
@@ -0,0 +1,26 @@
1
+ import * as z from "zod/v4";
2
+ import type { OcxConfig } from "../../types";
3
+
4
+ /** Opt-in allowances for canonical ChatGPT reasoning replay; absence keeps existing cleanup. */
5
+ export const nativeReasoningRetentionSchema = z.object({
6
+ modelSwitch: z.boolean().optional(),
7
+ accountSwitch: z.boolean().optional(),
8
+ }).strict();
9
+
10
+ export function nativeReasoningRetentionConfigError(value: unknown): string | null {
11
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
12
+ const retention = (value as Record<string, unknown>).nativeReasoningRetention;
13
+ return retention === undefined || nativeReasoningRetentionSchema.safeParse(retention).success
14
+ ? null : "schema_invalid: nativeReasoningRetention: accepts only optional boolean modelSwitch and accountSwitch fields";
15
+ }
16
+
17
+ /** Resolve malformed synthetic inputs the same way as degraded disk config: both off. */
18
+ export function resolveNativeReasoningRetention(
19
+ config: Pick<OcxConfig, "nativeReasoningRetention">,
20
+ ): { modelSwitch: boolean; accountSwitch: boolean } {
21
+ const parsed = nativeReasoningRetentionSchema.safeParse(config.nativeReasoningRetention);
22
+ return {
23
+ modelSwitch: parsed.success && parsed.data.modelSwitch === true,
24
+ accountSwitch: parsed.success && parsed.data.accountSwitch === true,
25
+ };
26
+ }
package/src/config.ts CHANGED
@@ -162,6 +162,7 @@ import { persistConfigUnlocked, readRawConfigJson } from "./config/persist-unloc
162
162
  import { withConfigMutationLockSync, bumpGenerationForCooperatingConfigWrite } from "./config/mutation-lock";
163
163
  import { getDefaultConfig } from "./config/proxy-env";
164
164
  import { configSchema } from "./config/schema/config-schema";
165
+ import { sanitizeOpenCodeOverridesForLoad } from "./config/opencode-wire-upgrade";
165
166
  import {
166
167
  hardenExistingSecret,
167
168
  normalizeApiKeyIds,
@@ -222,6 +223,7 @@ export function loadConfig(): OcxConfig {
222
223
  try {
223
224
  const raw = readFileSync(configPath, "utf-8").replace(/^\uFEFF/, "");
224
225
  const parsed = JSON.parse(raw);
226
+ sanitizeOpenCodeOverridesForLoad(parsed);
225
227
  sanitizeAliasesForLoad(parsed);
226
228
  sanitizeReasoningPinsForLoad(parsed);
227
229
  sanitizeModelDisplayNamesForLoad(parsed);