@bitkyc08/opencodex 2.80.0 → 2.82.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 (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -0,0 +1,281 @@
1
+ import { readBoundedResponseBytes } from "../lib/bounded-body";
2
+ import {
3
+ providerOutboundPost,
4
+ providerRedirectError,
5
+ } from "../lib/provider-outbound";
6
+ import {
7
+ isKeychainReference,
8
+ keychainReferenceBelongsToProvider,
9
+ resolveProviderApiKey,
10
+ } from "../providers/api-key-resolve";
11
+ import { providerMatchesRegistryTransport } from "../providers/registry";
12
+ import type { OcxConfig, OcxProviderConfig } from "../types";
13
+ import type { JevDecision, ResolveJevDecisionOptions } from "./jev";
14
+ import {
15
+ isSystemOneEndpoint,
16
+ jevDecisionEndpointUrl,
17
+ JEV_DECISION_TIMEOUT_DEFAULT_MS,
18
+ JEV_DECISION_TIMEOUT_MAX_MS,
19
+ JEV_DECISION_TIMEOUT_MIN_MS,
20
+ } from "./jev-decision-contract";
21
+
22
+ export const JEV_PROVIDER_ID = "jev";
23
+ export const JEV_API_URL = "https://api.typesafe.ai/v1/systemone";
24
+ export const JEV_MODEL = "jev-latest";
25
+
26
+ /** Environment names that hold TypeSafe credentials; a self-hosted row may never reference them. */
27
+ const TYPESAFE_ENV_KEYS = new Set(["TYPESAFE_API_KEY", "JEV_API_KEY"]);
28
+ export const JEV_MAX_REQUEST_BYTES = 65_536;
29
+ export const JEV_MAX_RESPONSE_BYTES = 65_536;
30
+ const JEV_OUTBOUND_DEPENDENCIES = {
31
+ isCanonicalUrl: (name: string, url: string) => name === JEV_PROVIDER_ID && url === JEV_API_URL,
32
+ // Self-hosted decision models (Ollama tev1) listen on plain HTTP loopback. The outbound wrapper
33
+ // still requires the row's own allowPrivateNetwork and a literal local address for that.
34
+ allowLocalCleartextPost: true,
35
+ };
36
+
37
+ /** Every non-applied outcome of one exchange: a local refusal, a transport failure or an invalid answer. */
38
+ export type JevDecisionFailureGate = Exclude<JevDecision["gate"], "apply">;
39
+
40
+ /**
41
+ * Transport options shared with the route resolver. `signal` is the caller's cancellation and always
42
+ * rejects with its own reason; `timeoutMs` is the separate decision deadline that yields a `timeout` gate.
43
+ */
44
+ export type JevServiceExchangeOptions = Pick<ResolveJevDecisionOptions,
45
+ "config" | "decisionProvider" | "isDestinationAllowed" | "timeoutMs" | "signal" | "post">;
46
+
47
+ /** Request shape only: question builders never receive the destination or credential. */
48
+ export interface JevDecisionEndpointShape {
49
+ model: string;
50
+ descriptiveCriteria: boolean;
51
+ }
52
+
53
+ /** The decision deadline: the configured value when in bounds, otherwise the four-second default. */
54
+ export function jevDecisionTimeoutMs(value: number | undefined): number {
55
+ return value !== undefined
56
+ && Number.isInteger(value)
57
+ && value >= JEV_DECISION_TIMEOUT_MIN_MS
58
+ && value <= JEV_DECISION_TIMEOUT_MAX_MS
59
+ ? value
60
+ : JEV_DECISION_TIMEOUT_DEFAULT_MS;
61
+ }
62
+
63
+ /** Retain a registry-matching JEV row's transport policy; ignore a retargeted row for hosted requests. */
64
+ function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
65
+ const configured = config.providers[JEV_PROVIDER_ID];
66
+ if (configured && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured)) return configured;
67
+ return {
68
+ adapter: "jev-decision",
69
+ baseUrl: JEV_API_URL,
70
+ authMode: "key",
71
+ liveModels: false,
72
+ };
73
+ }
74
+
75
+ interface JevDecisionEndpoint {
76
+ name: string;
77
+ provider: OcxProviderConfig;
78
+ url: string;
79
+ model: string;
80
+ apiKey: string | undefined;
81
+ /** Self-hosted System One services accept only string option descriptions. */
82
+ descriptiveCriteria: boolean;
83
+ }
84
+
85
+ /** Recognize the $NAME and ${NAME} credential references before applying self-hosted ownership rules. */
86
+ function envReferenceName(value: string): string | undefined {
87
+ const braced = /^\$\{(\w+)\}$/.exec(value);
88
+ if (braced) return braced[1];
89
+ return value.startsWith("$") ? value.slice(1) : undefined;
90
+ }
91
+
92
+ /**
93
+ * A self-hosted row may carry only its own secret: never a reference to the TypeSafe environment
94
+ * keys, and never a keychain entry that belongs to another provider. `null` means refused.
95
+ */
96
+ function selfHostedApiKey(name: string, apiKey: string | undefined): string | undefined | null {
97
+ if (!apiKey) return undefined;
98
+ const envName = envReferenceName(apiKey);
99
+ if (envName !== undefined && TYPESAFE_ENV_KEYS.has(envName)) return null;
100
+ if (isKeychainReference(apiKey) && !keychainReferenceBelongsToProvider(apiKey, name)) return null;
101
+ return resolveProviderApiKey(apiKey)?.trim() || undefined;
102
+ }
103
+
104
+ /**
105
+ * Resolve where one decision request goes and which credential it may carry.
106
+ *
107
+ * The `jev` id stays pinned to the canonical TypeSafe URL and `jev-latest`: its row key is used
108
+ * only while the row still matches the registry transport, and the environment fallbacks exist
109
+ * only for that URL. A retargeted `jev` row therefore keeps today's behavior instead of becoming a
110
+ * custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
111
+ * full HTTPS decision endpoint (or a local HTTP `/systemone` endpoint) and which names its own
112
+ * model; only its own key may accompany it, so no TypeSafe credential can reach a self-hosted
113
+ * service. The URL is `jevDecisionEndpointUrl`, shared with the management surfaces: an HTTPS
114
+ * path is sent exactly as configured and only `/systemone` normalizes trailing slashes.
115
+ * `undefined` means no usable decision service (reported through the existing `missing_key`
116
+ * gate); `null` means the request's destination scope refused it before any credential access.
117
+ */
118
+ function jevDecisionEndpoint(
119
+ config: OcxConfig,
120
+ decisionProvider: string,
121
+ isDestinationAllowed?: ResolveJevDecisionOptions["isDestinationAllowed"],
122
+ ): JevDecisionEndpoint | undefined | null {
123
+ const configured = Object.hasOwn(config.providers, decisionProvider)
124
+ ? config.providers[decisionProvider]
125
+ : undefined;
126
+ if (configured?.disabled === true) return undefined;
127
+ if (decisionProvider === JEV_PROVIDER_ID) {
128
+ if (isDestinationAllowed?.(JEV_PROVIDER_ID, JEV_MODEL) === false) return null;
129
+ const configuredOwnsJev = configured
130
+ && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
131
+ const apiKey = (
132
+ configuredOwnsJev ? resolveProviderApiKey(configured.apiKey)?.trim() : undefined
133
+ ) || process.env.TYPESAFE_API_KEY?.trim()
134
+ || process.env.JEV_API_KEY?.trim();
135
+ if (!apiKey) return undefined;
136
+ return {
137
+ name: JEV_PROVIDER_ID,
138
+ provider: canonicalJevProvider(config),
139
+ url: JEV_API_URL,
140
+ model: JEV_MODEL,
141
+ apiKey,
142
+ descriptiveCriteria: false,
143
+ };
144
+ }
145
+ if (configured?.adapter !== "jev-decision" || typeof configured.baseUrl !== "string") return undefined;
146
+ const url = jevDecisionEndpointUrl(configured.baseUrl);
147
+ if (!url || !isSystemOneEndpoint(url)) return undefined;
148
+ // `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
149
+ const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
150
+ if (!model) return undefined;
151
+ if (isDestinationAllowed?.(decisionProvider, model) === false) return null;
152
+ const apiKey = selfHostedApiKey(decisionProvider, configured.apiKey);
153
+ if (apiKey === null) return undefined;
154
+ return {
155
+ name: decisionProvider,
156
+ provider: configured,
157
+ url,
158
+ model,
159
+ apiKey,
160
+ descriptiveCriteria: true,
161
+ };
162
+ }
163
+
164
+ /** Run the question builder: a refusal keeps its gate; a throw or an over-cap body is `invalid`. */
165
+ function prepareRequestBody(
166
+ prepare: (endpoint: JevDecisionEndpointShape) => { body: string } | JevDecisionFailureGate,
167
+ shape: JevDecisionEndpointShape,
168
+ ): { body: string } | { gate: JevDecisionFailureGate } {
169
+ try {
170
+ const prepared = prepare(shape);
171
+ if (typeof prepared === "string") return { gate: prepared };
172
+ const body = prepared.body;
173
+ if (new TextEncoder().encode(body).byteLength > JEV_MAX_REQUEST_BYTES) return { gate: "invalid" };
174
+ return { body };
175
+ } catch {
176
+ return { gate: "invalid" };
177
+ }
178
+ }
179
+
180
+ /**
181
+ * One bounded System One round-trip, independent of the decision question and its choice policy.
182
+ * Destination authorization precedes credential access and `prepare`; request bytes, deadline,
183
+ * redirects and UTF-8 JSON are bounded here. `parse` validates the question's answer inside the
184
+ * cancellation boundary. Local preparation/parser failures are `invalid`.
185
+ *
186
+ * This function owns caller cancellation: an aborted `options.signal` rejects with its reason by
187
+ * identity, never as a gate. It is checked before endpoint, credential or `prepare` work, after
188
+ * endpoint resolution and preparation (including failures), after POST, redirect inspection, HTTP
189
+ * error-body cleanup and response reads, and when `parse` returns or throws. Only the caller's signal counts; expiry of
190
+ * the separate decision deadline remains a `timeout` gate.
191
+ */
192
+ export async function exchangeJevDecision<T>(
193
+ options: JevServiceExchangeOptions,
194
+ prepare: (endpoint: JevDecisionEndpointShape) => { body: string } | JevDecisionFailureGate,
195
+ parse: (payload: unknown) => T,
196
+ ): Promise<{ value: T } | { gate: JevDecisionFailureGate }> {
197
+ if (options.signal?.aborted) throw options.signal.reason;
198
+
199
+ let endpoint: JevDecisionEndpoint | undefined | null;
200
+ try {
201
+ endpoint = jevDecisionEndpoint(options.config, options.decisionProvider ?? JEV_PROVIDER_ID, options.isDestinationAllowed);
202
+ } catch (error) {
203
+ // Authorization and credential callbacks may cancel the caller and then throw; cancellation wins.
204
+ if (options.signal?.aborted) throw options.signal.reason;
205
+ throw error;
206
+ }
207
+ if (options.signal?.aborted) throw options.signal.reason;
208
+ if (endpoint === null) return { gate: "invalid" };
209
+ if (!endpoint) return { gate: "missing_key" };
210
+
211
+ const request = prepareRequestBody(prepare, { model: endpoint.model, descriptiveCriteria: endpoint.descriptiveCriteria });
212
+ if (options.signal?.aborted) throw options.signal.reason;
213
+ if ("gate" in request) return request;
214
+ const requestBody = request.body;
215
+
216
+ const timeoutMs = jevDecisionTimeoutMs(options.timeoutMs);
217
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
218
+ const signal = options.signal
219
+ ? AbortSignal.any([options.signal, timeoutSignal])
220
+ : timeoutSignal;
221
+ const post = options.post ?? providerOutboundPost;
222
+
223
+ try {
224
+ const response = await post(
225
+ endpoint.name,
226
+ endpoint.provider,
227
+ endpoint.url,
228
+ {
229
+ headers: {
230
+ ...(endpoint.apiKey ? { Authorization: `Bearer ${endpoint.apiKey}` } : {}),
231
+ "Content-Type": "application/json",
232
+ },
233
+ body: requestBody,
234
+ signal,
235
+ },
236
+ JEV_OUTBOUND_DEPENDENCIES,
237
+ );
238
+ if (options.signal?.aborted) throw options.signal.reason;
239
+
240
+ const redirectError = await providerRedirectError(response, endpoint.url);
241
+ if (options.signal?.aborted) throw options.signal.reason;
242
+ if (redirectError) return { gate: "redirect" };
243
+ if (!response.ok) {
244
+ try { void response.body?.cancel().catch(() => undefined); } catch { /* best effort */ }
245
+ if (options.signal?.aborted) throw options.signal.reason;
246
+ return { gate: "http" };
247
+ }
248
+
249
+ const bounded = await readBoundedResponseBytes(response, {
250
+ maxBytes: JEV_MAX_RESPONSE_BYTES,
251
+ signal,
252
+ });
253
+ if (options.signal?.aborted) throw options.signal.reason;
254
+ if (bounded.oversized) return { gate: "malformed" };
255
+
256
+ let payload: unknown;
257
+ try {
258
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bounded.bytes);
259
+ payload = JSON.parse(text);
260
+ } catch {
261
+ return { gate: "malformed" };
262
+ }
263
+
264
+ let parsed: T;
265
+ try {
266
+ parsed = parse(payload);
267
+ } catch {
268
+ if (options.signal?.aborted) throw options.signal.reason;
269
+ return { gate: "invalid" };
270
+ }
271
+ if (options.signal?.aborted) throw options.signal.reason;
272
+ return { value: parsed };
273
+ } catch (error) {
274
+ if (options.signal?.aborted) throw options.signal.reason;
275
+ if (timeoutSignal.aborted
276
+ || (error instanceof DOMException && error.name === "TimeoutError")) {
277
+ return { gate: "timeout" };
278
+ }
279
+ return { gate: "network" };
280
+ }
281
+ }
package/src/combos/jev.ts CHANGED
@@ -1,41 +1,26 @@
1
- import { readBoundedResponseBytes } from "../lib/bounded-body";
1
+ import type { providerOutboundPost } from "../lib/provider-outbound";
2
+ import type { OcxComboDefaultEffort, OcxConfig } from "../types";
2
3
  import {
3
- providerOutboundPost,
4
- providerRedirectError,
5
- } from "../lib/provider-outbound";
6
- import {
7
- isKeychainReference,
8
- keychainReferenceBelongsToProvider,
9
- resolveProviderApiKey,
10
- } from "../providers/api-key-resolve";
11
- import { providerMatchesRegistryTransport } from "../providers/registry";
12
- import type { OcxComboDefaultEffort, OcxConfig, OcxProviderConfig } from "../types";
13
- import {
14
- isSystemOneEndpoint,
15
- JEV_DECISION_TIMEOUT_DEFAULT_MS,
16
- JEV_DECISION_TIMEOUT_MAX_MS,
17
- JEV_DECISION_TIMEOUT_MIN_MS,
18
4
  JEV_MAX_CANDIDATE_FIELD_CHARS,
19
5
  } from "./types";
20
6
 
21
- export const JEV_PROVIDER_ID = "jev";
22
- export const JEV_API_URL = "https://api.typesafe.ai/v1/systemone";
23
- export const JEV_MODEL = "jev-latest";
7
+ import {
8
+ exchangeJevDecision,
9
+ JEV_PROVIDER_ID,
10
+ } from "./jev-service-exchange";
11
+ export {
12
+ JEV_API_URL,
13
+ JEV_MODEL,
14
+ JEV_PROVIDER_ID,
15
+ JEV_MAX_REQUEST_BYTES,
16
+ JEV_MAX_RESPONSE_BYTES,
17
+ jevDecisionTimeoutMs,
18
+ } from "./jev-service-exchange";
24
19
 
25
20
  export const JEV_MAX_CANDIDATES = 64;
26
21
  /** Ollama's System One choice questions accept 2..26 options; other services share the floor. */
27
22
  const SELF_HOSTED_MIN_OPTIONS = 2;
28
23
  const SELF_HOSTED_MAX_OPTIONS = 26;
29
- /** Environment names that hold TypeSafe credentials; a self-hosted row may never reference them. */
30
- const TYPESAFE_ENV_KEYS = new Set(["TYPESAFE_API_KEY", "JEV_API_KEY"]);
31
- export const JEV_MAX_REQUEST_BYTES = 65_536;
32
- export const JEV_MAX_RESPONSE_BYTES = 65_536;
33
- const JEV_OUTBOUND_DEPENDENCIES = {
34
- isCanonicalUrl: (name: string, url: string) => name === JEV_PROVIDER_ID && url === JEV_API_URL,
35
- // Self-hosted decision models (Ollama tev1) listen on plain HTTP loopback. The outbound wrapper
36
- // still requires the row's own allowPrivateNetwork and a literal local address for that.
37
- allowLocalCleartextPost: true,
38
- };
39
24
 
40
25
  const TASK_CHARS = 500;
41
26
  const TASK_HEAD_CHARS = 320;
@@ -383,6 +368,7 @@ function hasImageContent(item: Record<string, unknown>): boolean {
383
368
  return item.content.some(part => isRecord(part) && (part.type === "input_image" || part.type === "image_url"));
384
369
  }
385
370
 
371
+ /** Extract bounded task/assistant/tool evidence, omit protected envelopes, and attach configured target notes. */
386
372
  export function buildJevState(body: unknown, candidates: readonly JevCandidate[] = []): Record<string, unknown> {
387
373
  const input = isRecord(body) ? body.input : undefined;
388
374
  let task = "";
@@ -444,6 +430,7 @@ export function buildJevState(body: unknown, candidates: readonly JevCandidate[]
444
430
  };
445
431
  }
446
432
 
433
+ /** Require task text, an image signal, or a tool-output tail; assistant context and operator notes alone do not suffice. */
447
434
  export function hasJevDecisionState(state: Record<string, unknown>): boolean {
448
435
  if (typeof state.task === "string" && state.task.trim()) return true;
449
436
  if (isRecord(state.signals) && state.signals.has_image === true) return true;
@@ -475,6 +462,7 @@ function candidateOptions(candidates: readonly JevCandidate[]): Map<string, JevR
475
462
  return options;
476
463
  }
477
464
 
465
+ /** Check candidate count and field lengths before state extraction; the exchange separately caps serialized bytes. */
478
466
  export function candidatesFitRequestBounds(candidates: readonly JevCandidate[]): boolean {
479
467
  if (candidates.length > JEV_MAX_CANDIDATES) return false;
480
468
  return candidates.every(candidate => [candidate.key, candidate.provider, candidate.model]
@@ -549,6 +537,7 @@ export function buildJevRouteQuestion(
549
537
  };
550
538
  }
551
539
 
540
+ /** Project only allowlisted, nonnegative safe-integer usage counters; omit missing or wholly invalid usage. */
552
541
  export function jevUsage(payload: Record<string, unknown>): Record<string, number> | undefined {
553
542
  if (!isRecord(payload.usage)) return undefined;
554
543
  const usage: Record<string, number> = {};
@@ -559,6 +548,7 @@ export function jevUsage(payload: Record<string, unknown>): Record<string, numbe
559
548
  return Object.keys(usage).length > 0 ? usage : undefined;
560
549
  }
561
550
 
551
+ /** Validate an allowlisted route choice and any complete probability distribution; throw on invalid answers. */
562
552
  export function parseJevDecision(
563
553
  payload: unknown,
564
554
  candidates: readonly JevCandidate[],
@@ -611,6 +601,7 @@ export function parseJevDecision(
611
601
  };
612
602
  }
613
603
 
604
+ /** Preserve the caller's eligible target/effort fallback while recording the failed gate, backend, and latency. */
614
605
  export function fallbackDecision(
615
606
  fallback: ResolveJevDecisionOptions["fallback"],
616
607
  gate: Exclude<JevDecision["gate"], "apply">,
@@ -620,113 +611,6 @@ export function fallbackDecision(
620
611
  return { backend, ...fallback, gate, latencyMs };
621
612
  }
622
613
 
623
- /** The decision deadline: the configured value when in bounds, otherwise the four-second default. */
624
- export function jevDecisionTimeoutMs(value: number | undefined): number {
625
- return value !== undefined
626
- && Number.isInteger(value)
627
- && value >= JEV_DECISION_TIMEOUT_MIN_MS
628
- && value <= JEV_DECISION_TIMEOUT_MAX_MS
629
- ? value
630
- : JEV_DECISION_TIMEOUT_DEFAULT_MS;
631
- }
632
-
633
- function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
634
- const configured = config.providers[JEV_PROVIDER_ID];
635
- if (configured && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured)) return configured;
636
- return {
637
- adapter: "jev-decision",
638
- baseUrl: JEV_API_URL,
639
- authMode: "key",
640
- liveModels: false,
641
- };
642
- }
643
-
644
- interface JevDecisionEndpoint {
645
- name: string;
646
- provider: OcxProviderConfig;
647
- url: string;
648
- model: string;
649
- apiKey: string | undefined;
650
- /** Self-hosted System One services accept only string option descriptions. */
651
- descriptiveCriteria: boolean;
652
- }
653
-
654
- function envReferenceName(value: string): string | undefined {
655
- const braced = /^\$\{(\w+)\}$/.exec(value);
656
- if (braced) return braced[1];
657
- return value.startsWith("$") ? value.slice(1) : undefined;
658
- }
659
-
660
- /**
661
- * A self-hosted row may carry only its own secret: never a reference to the TypeSafe environment
662
- * keys, and never a keychain entry that belongs to another provider. `null` means refused.
663
- */
664
- function selfHostedApiKey(name: string, apiKey: string | undefined): string | undefined | null {
665
- if (!apiKey) return undefined;
666
- const envName = envReferenceName(apiKey);
667
- if (envName !== undefined && TYPESAFE_ENV_KEYS.has(envName)) return null;
668
- if (isKeychainReference(apiKey) && !keychainReferenceBelongsToProvider(apiKey, name)) return null;
669
- return resolveProviderApiKey(apiKey)?.trim() || undefined;
670
- }
671
-
672
- /**
673
- * Resolve where one decision request goes and which credential it may carry.
674
- *
675
- * The `jev` id stays pinned to the canonical TypeSafe URL and `jev-latest`: its row key is used
676
- * only while the row still matches the registry transport, and the environment fallbacks exist
677
- * only for that URL. A retargeted `jev` row therefore keeps today's behavior instead of becoming a
678
- * custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
679
- * `/systemone` endpoint and which names its own model; only its own key may accompany it, so no
680
- * TypeSafe credential can reach a self-hosted service. `undefined` means no usable decision
681
- * service (reported through the existing `missing_key` gate); `null` means the request's
682
- * destination scope refused it before any credential access.
683
- */
684
- function jevDecisionEndpoint(
685
- config: OcxConfig,
686
- decisionProvider: string,
687
- isDestinationAllowed?: ResolveJevDecisionOptions["isDestinationAllowed"],
688
- ): JevDecisionEndpoint | undefined | null {
689
- const configured = Object.hasOwn(config.providers, decisionProvider)
690
- ? config.providers[decisionProvider]
691
- : undefined;
692
- if (configured?.disabled === true) return undefined;
693
- if (decisionProvider === JEV_PROVIDER_ID) {
694
- if (isDestinationAllowed?.(JEV_PROVIDER_ID, JEV_MODEL) === false) return null;
695
- const configuredOwnsJev = configured
696
- && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
697
- const apiKey = (
698
- configuredOwnsJev ? resolveProviderApiKey(configured.apiKey)?.trim() : undefined
699
- ) || process.env.TYPESAFE_API_KEY?.trim()
700
- || process.env.JEV_API_KEY?.trim();
701
- if (!apiKey) return undefined;
702
- return {
703
- name: JEV_PROVIDER_ID,
704
- provider: canonicalJevProvider(config),
705
- url: JEV_API_URL,
706
- model: JEV_MODEL,
707
- apiKey,
708
- descriptiveCriteria: false,
709
- };
710
- }
711
- if (configured?.adapter !== "jev-decision" || typeof configured.baseUrl !== "string") return undefined;
712
- const url = configured.baseUrl.trim().replace(/\/+$/, "");
713
- if (!url || !isSystemOneEndpoint(url)) return undefined;
714
- // `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
715
- const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
716
- if (!model) return undefined;
717
- if (isDestinationAllowed?.(decisionProvider, model) === false) return null;
718
- const apiKey = selfHostedApiKey(decisionProvider, configured.apiKey);
719
- if (apiKey === null) return undefined;
720
- return {
721
- name: decisionProvider,
722
- provider: configured,
723
- url,
724
- model,
725
- apiKey,
726
- descriptiveCriteria: true,
727
- };
728
- }
729
-
730
614
  /**
731
615
  * Ask the configured JEV decision service for one allowlisted target/effort decision.
732
616
  *
@@ -744,98 +628,29 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
744
628
  if (options.candidates.length === 0) return failed("no_choices");
745
629
  if (!candidatesFitRequestBounds(options.candidates)) return failed("invalid");
746
630
 
747
- const endpoint = jevDecisionEndpoint(options.config, options.decisionProvider ?? JEV_PROVIDER_ID, options.isDestinationAllowed);
748
- if (endpoint === null) return failed("invalid");
749
- if (!endpoint) return failed("missing_key");
750
-
751
- let requestBody: string;
752
- try {
631
+ const exchanged = await exchangeJevDecision(options, (endpoint) => {
753
632
  if (endpoint.descriptiveCriteria) {
754
633
  // Self-hosted choice questions accept 2..26 options; decide locally instead of spending a
755
634
  // round-trip on a request the service will refuse.
756
635
  const optionCount = candidateOptions(options.candidates).size;
757
- if (optionCount < SELF_HOSTED_MIN_OPTIONS) return failed("no_choices");
758
- if (optionCount > SELF_HOSTED_MAX_OPTIONS) return failed("invalid");
636
+ if (optionCount < SELF_HOSTED_MIN_OPTIONS) return "no_choices";
637
+ if (optionCount > SELF_HOSTED_MAX_OPTIONS) return "invalid";
759
638
  }
760
639
  const state = buildJevState(options.body, options.candidates);
761
- if (!hasJevDecisionState(state)) return failed("no_state");
762
- requestBody = JSON.stringify({
640
+ if (!hasJevDecisionState(state)) return "no_state";
641
+ return { body: JSON.stringify({
763
642
  model: endpoint.model,
764
643
  state,
765
644
  questions: buildJevRouteQuestion(options.candidates, { descriptiveCriteria: endpoint.descriptiveCriteria }),
766
- });
767
- if (new TextEncoder().encode(requestBody).byteLength > JEV_MAX_REQUEST_BYTES) return failed("invalid");
768
- } catch {
769
- return failed("invalid");
770
- }
771
-
772
- const timeoutMs = jevDecisionTimeoutMs(options.timeoutMs);
773
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
774
- const signal = options.signal
775
- ? AbortSignal.any([options.signal, timeoutSignal])
776
- : timeoutSignal;
777
- const post = options.post ?? providerOutboundPost;
778
-
779
- try {
780
- const response = await post(
781
- endpoint.name,
782
- endpoint.provider,
783
- endpoint.url,
784
- {
785
- headers: {
786
- ...(endpoint.apiKey ? { Authorization: `Bearer ${endpoint.apiKey}` } : {}),
787
- "Content-Type": "application/json",
788
- },
789
- body: requestBody,
790
- signal,
791
- },
792
- JEV_OUTBOUND_DEPENDENCIES,
793
- );
794
- if (options.signal?.aborted) throw options.signal.reason;
795
-
796
- const redirectError = await providerRedirectError(response, endpoint.url);
797
- if (redirectError) return failed("redirect");
798
- if (!response.ok) {
799
- try { void response.body?.cancel().catch(() => undefined); } catch { /* best effort */ }
800
- return failed("http");
801
- }
802
-
803
- const bounded = await readBoundedResponseBytes(response, {
804
- maxBytes: JEV_MAX_RESPONSE_BYTES,
805
- signal,
806
- });
807
- if (options.signal?.aborted) throw options.signal.reason;
808
- if (bounded.oversized) return failed("malformed");
809
-
810
- let payload: unknown;
811
- try {
812
- const text = new TextDecoder("utf-8", { fatal: true }).decode(bounded.bytes);
813
- payload = JSON.parse(text);
814
- } catch {
815
- return failed("malformed");
816
- }
817
-
818
- let parsed: ReturnType<typeof parseJevDecision>;
819
- try {
820
- parsed = parseJevDecision(payload, options.candidates);
821
- } catch {
822
- return failed("invalid");
823
- }
824
- if (options.signal?.aborted) throw options.signal.reason;
825
- return {
826
- backend,
827
- ...parsed,
828
- gate: "apply",
829
- latencyMs: Math.max(0, now() - startedAt),
830
- };
831
- } catch (error) {
832
- if (options.signal?.aborted) throw options.signal.reason;
833
- if (timeoutSignal.aborted
834
- || (error instanceof DOMException && error.name === "TimeoutError")) {
835
- return failed("timeout");
836
- }
837
- return failed("network");
838
- }
645
+ }) };
646
+ }, (payload) => parseJevDecision(payload, options.candidates));
647
+ if ("gate" in exchanged) return failed(exchanged.gate);
648
+ return {
649
+ backend,
650
+ ...exchanged.value,
651
+ gate: "apply",
652
+ latencyMs: Math.max(0, now() - startedAt),
653
+ };
839
654
  }
840
655
 
841
656
  export type JevDecisionProbeResult =
@@ -302,7 +302,7 @@ export function comboConfigIssues(
302
302
  } else if (!isSystemOneEndpoint(String(providers[decisionProvider]?.baseUrl ?? ""))) {
303
303
  issues.push({
304
304
  path: ["decisionProvider"],
305
- message: `decisionProvider "${decisionProvider}" baseUrl must be the full decision endpoint ending in /systemone`,
305
+ message: `decisionProvider "${decisionProvider}" baseUrl must be a full HTTPS decision endpoint or an HTTP /systemone endpoint`,
306
306
  });
307
307
  } else if (options.requireUsableDecisionService && providers[decisionProvider]?.disabled === true) {
308
308
  issues.push({
@@ -304,7 +304,7 @@ function atomicWriteFileToTarget(
304
304
  }
305
305
  try { chmodSync(tempPath, 0o600); } catch { /* platform may ignore chmod */ }
306
306
  },
307
- rename: renameAtomicFile,
307
+ rename: (source, destination) => renameAtomicFile(source, destination, undefined, "config", hooks),
308
308
  truncate: tempPath => truncateSync(tempPath, 0),
309
309
  unlink: unlinkSync,
310
310
  };
@@ -0,0 +1,10 @@
1
+ /** Process-local opt-in for cross-turn Codex WebSocket reuse. Default off. */
2
+ let reuseAcrossTurns = false;
3
+
4
+ export function setCodexWsReuseAcrossTurns(enabled: boolean): void {
5
+ reuseAcrossTurns = enabled;
6
+ }
7
+
8
+ export function codexWsReuseAcrossTurnsEnabled(): boolean {
9
+ return reuseAcrossTurns;
10
+ }