@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Effective export metadata for custom `/api/models` rows.
3
+ *
4
+ * A custom row's own fields are the operator's stored OVERRIDE. `/api/models` returns them
5
+ * verbatim because the edit dialog must show what the user set — including an explicit empty
6
+ * "no reasoning" ladder — not what the provider happens to advertise today. But a client
7
+ * export has to serialize what the proxy will actually route: the canonical resolved catalog
8
+ * metadata for the same `provider/modelId`. `listManagementModelRows` resolves that
9
+ * projection here and attaches it additively, so the editor's raw fields and the exporter's
10
+ * effective fields can never overwrite each other.
11
+ *
12
+ * Sources are the ones the routed-row projection already uses — the gathered catalog row for
13
+ * the same slug (the gather materializes custom rows with provider/native inheritance), the
14
+ * registry-enriched provider config, and the ladder resolution in `model-rows.ts`. No
15
+ * independent provider fact tables, and no per-model fetch: everything comes from the roster
16
+ * and config already in hand.
17
+ */
18
+ import { enrichProviderFromRegistry } from "../../providers/derive";
19
+ import { modelRecordValue } from "../../reasoning-effort";
20
+ import { configuredReasoningSummarySupport } from "../../codex/catalog/model-hints";
21
+ import type { CatalogModel } from "../../codex/catalog";
22
+ import type { OcxConfig } from "../../types";
23
+ import {
24
+ knownReasoningSupport,
25
+ knownToolsSupport,
26
+ type EffectiveModelExportMetadata,
27
+ } from "../../clients/config-export/contracts";
28
+
29
+ /** The stored-override fields a custom row carries, exactly as `config.customModels` writes them. */
30
+ export interface CustomRowOverrideSource {
31
+ provider: string;
32
+ id: string;
33
+ contextWindow?: number;
34
+ inputModalities?: string[];
35
+ /** Explicit ladder; `[]` is a declaration ("no rungs"), not an absence. */
36
+ reasoningEfforts?: string[];
37
+ defaultReasoningEffort?: string;
38
+ }
39
+
40
+ /**
41
+ * Resolve the effective export metadata for one custom row.
42
+ *
43
+ * Explicit custom overrides win; resolved gathered facts precede provider-config fallbacks.
44
+ * `effectiveReasoningEfforts` is
45
+ * supplied by the caller because its resolution (declared ladder first, then the enriched
46
+ * provider maps, then the pinned native table, then the catalog hints) lives with the other
47
+ * ladder readers in `model-rows.ts`; here it is an input, resolved once per row.
48
+ */
49
+ export function customRowExportMetadata(
50
+ config: OcxConfig,
51
+ source: CustomRowOverrideSource,
52
+ catalogRow: CatalogModel | undefined,
53
+ effectiveReasoningEfforts: string[] | undefined,
54
+ ): EffectiveModelExportMetadata {
55
+ const provider = config.providers[source.provider];
56
+ // The same registry hydration the other per-model readers at this boundary perform, so a
57
+ // custom row whose provider declares facts only in the registry still inherits them. The
58
+ // per-model records are cloned first because the enrichment fills and folds them in place.
59
+ const enriched = provider === undefined ? undefined : {
60
+ ...provider,
61
+ ...(provider.modelContextWindows ? { modelContextWindows: { ...provider.modelContextWindows } } : {}),
62
+ ...(provider.modelInputModalities ? { modelInputModalities: { ...provider.modelInputModalities } } : {}),
63
+ ...(provider.modelCapabilities ? { modelCapabilities: { ...provider.modelCapabilities } } : {}),
64
+ ...(provider.modelReasoningEfforts ? { modelReasoningEfforts: { ...provider.modelReasoningEfforts } } : {}),
65
+ ...(provider.modelDefaultReasoningEfforts
66
+ ? { modelDefaultReasoningEfforts: { ...provider.modelDefaultReasoningEfforts } }
67
+ : {}),
68
+ };
69
+ if (enriched) enrichProviderFromRegistry(source.provider, enriched);
70
+
71
+ // Gathered resolved context first after the explicit override: the gather already ran the
72
+ // canonical precedence (operator maps, registry, native clamps) and may have applied a
73
+ // tighter cap than a config map read here would suggest. Re-reading the config maps ahead
74
+ // of it would duplicate that policy and could widen past an observed tighter window.
75
+ const contextWindow = source.contextWindow
76
+ ?? catalogRow?.contextWindow
77
+ ?? modelRecordValue(enriched?.modelContextWindows, source.id)
78
+ ?? enriched?.contextWindow;
79
+ // Same declaration chain `declaredModelInputModalities` reads, as the fallback when neither
80
+ // the override nor the gathered row says anything.
81
+ const declaredModalities = enriched === undefined ? undefined : (
82
+ enriched.modelCapabilities?.[source.id]?.inputModalities
83
+ ?? modelRecordValue(enriched.modelInputModalities, source.id)
84
+ );
85
+ const inputModalities = source.inputModalities ?? catalogRow?.inputModalities ?? declaredModalities;
86
+ // The gathered row carries what the materializer already resolved; for a rosterless row the
87
+ // same reader the materializer uses answers from the provider config, so both paths agree.
88
+ const supportsReasoningSummaries = catalogRow?.supportsReasoningSummaries
89
+ ?? configuredReasoningSummarySupport(provider, source.id);
90
+ const defaultReasoningEffort = declaredDefaultWithin(
91
+ effectiveReasoningEfforts,
92
+ defaultEffort(source, catalogRow, provider),
93
+ );
94
+ return {
95
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
96
+ ...(catalogRow?.maxInputTokens !== undefined ? { maxInputTokens: catalogRow.maxInputTokens } : {}),
97
+ ...(catalogRow?.maxOutputTokens !== undefined ? { maxTokens: catalogRow.maxOutputTokens } : {}),
98
+ ...(inputModalities !== undefined ? { inputModalities: [...inputModalities] } : {}),
99
+ ...(effectiveReasoningEfforts !== undefined ? { reasoningEfforts: [...effectiveReasoningEfforts] } : {}),
100
+ ...(defaultReasoningEffort !== undefined ? { defaultReasoningEffort } : {}),
101
+ ...(knownToolsSupport({
102
+ capabilities: catalogRow?.capabilities,
103
+ parallelToolCalls: catalogRow?.parallelToolCalls,
104
+ }) === true ? { supportsTools: true } : {}),
105
+ ...(knownReasoningSupport({ reasoningEfforts: effectiveReasoningEfforts, supportsReasoningSummaries }) === true
106
+ ? { supportsReasoning: true }
107
+ : {}),
108
+ ...(supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries } : {}),
109
+ };
110
+ }
111
+
112
+ /**
113
+ * The declared default, suppressed unless it is a member of the effective ladder. An empty
114
+ * ladder has no rung to default to, and a default the ladder does not contain describes an
115
+ * effort the proxy would never send — the same rule the settings route applies when it
116
+ * rejects a default "not in the declared reasoningEfforts ladder", applied here so an export
117
+ * never ships a default the row cannot act on.
118
+ */
119
+ function declaredDefaultWithin(
120
+ effectiveReasoningEfforts: string[] | undefined,
121
+ declared: string | undefined,
122
+ ): string | undefined {
123
+ if (declared === undefined) return undefined;
124
+ if (effectiveReasoningEfforts === undefined) return declared;
125
+ return effectiveReasoningEfforts.includes(declared) ? declared : undefined;
126
+ }
127
+
128
+ /**
129
+ * The declared default, and nothing else. The ladder-preference fallback
130
+ * (`effectiveModelDefaultReasoningEffort`'s medium → low → first) fabricates a default no
131
+ * source declared; it is right for the picker's own preference order but wrong for an export
132
+ * that claims to carry provider truth, so this projection stops at:
133
+ *
134
+ * 1. the explicit custom default (an override always wins),
135
+ * 2. the gathered catalog row's declared default (the gather only lets it ride when it is a
136
+ * member of the effective ladder),
137
+ * 3. the operator's per-model record — the same raw map the routed-row reader consults.
138
+ *
139
+ * `declaredDefaultWithin` then drops the answer when it is not a member of the effective
140
+ * ladder, empty ladder included.
141
+ */
142
+ function defaultEffort(
143
+ source: CustomRowOverrideSource,
144
+ catalogRow: CatalogModel | undefined,
145
+ provider: { modelDefaultReasoningEfforts?: Record<string, string> } | undefined,
146
+ ): string | undefined {
147
+ if (source.defaultReasoningEffort) return source.defaultReasoningEffort;
148
+ if (catalogRow?.defaultReasoningEffort) return catalogRow.defaultReasoningEffort;
149
+ return modelRecordValue(provider?.modelDefaultReasoningEfforts, source.id) ?? undefined;
150
+ }
@@ -9,6 +9,7 @@
9
9
  * Bodies are unchanged from their previous home; only `export` was added.
10
10
  */
11
11
  import type { CatalogModel } from "../../codex/catalog";
12
+ import { antigravityFamilyDisabled } from "../../providers/antigravity-effort-families";
12
13
  import { observeModelCacheRevision } from "../../codex/model-cache";
13
14
  import {
14
15
  captureExportConfigAdmission,
@@ -30,7 +31,13 @@ import {
30
31
  catalogModelEfforts,
31
32
  } from "../../codex/catalog";
32
33
  import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../../codex/catalog/native-models";
33
- import type { ExportModel } from "../../clients/config-export";
34
+ import {
35
+ knownReasoningSupport,
36
+ knownToolsSupport,
37
+ type EffectiveModelExportMetadata,
38
+ type ExportModel,
39
+ } from "../../clients/config-export/contracts";
40
+ import { customRowExportMetadata } from "./model-row-export-metadata";
34
41
  import { providerContextCap } from "../../providers/context-cap";
35
42
  import { isVisionReasoningEffort, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
36
43
  import { routedSlug, slugEquals } from "../../providers/slug-codec";
@@ -78,6 +85,28 @@ export type ManagementModelRow = Partial<CatalogModel> & {
78
85
  * config — so this compares values against what the model would inherit anyway.
79
86
  */
80
87
  reasoningOverridden?: boolean;
88
+ /**
89
+ * Tool support a canonical source positively asserts (native identity, a catalog row's
90
+ * `"tools"` capability, or the parallel-call opt-in — the same evidence
91
+ * `src/routing/capability.ts` counts). A row without positive evidence stays absent:
92
+ * unknown is not false.
93
+ */
94
+ supportsTools?: boolean;
95
+ /**
96
+ * True when a canonical source positively asserts reasoning (a non-empty effective ladder
97
+ * or delivered reasoning summaries). Absent means unknown, never "cannot reason": an empty
98
+ * ladder only says the effort is not adjustable.
99
+ */
100
+ supportsReasoning?: boolean;
101
+ /**
102
+ * Effective export projection for a row, resolved from canonical sources only — for a
103
+ * custom row the full effective view (the raw override fields above stay exactly what the
104
+ * editor wrote), for routed and native rows the authoritative declared default where the
105
+ * row's own `defaultReasoningEffort` is the picker's preference order instead. Additive on
106
+ * `/api/models`, and its PRESENCE is the authority signal: an absent field inside it means
107
+ * "no declared value", never "fall back to the row".
108
+ */
109
+ exportMetadata?: EffectiveModelExportMetadata;
81
110
  };
82
111
 
83
112
  /** Resolve the exact text and source shown for one routed discovered model. */
@@ -322,6 +351,13 @@ export async function listManagementModelRows(
322
351
  ...(row.autoCompactTokenLimit !== undefined
323
352
  ? { autoCompactTokenLimit: row.autoCompactTokenLimit }
324
353
  : {}),
354
+ // The pinned default above IS the declared default for a native row — no heuristic
355
+ // touches it — so the authoritative projection carries it verbatim and exists even
356
+ // when empty, which is what tells an export consumer "absence means no default"
357
+ // rather than "fall back to the row".
358
+ exportMetadata: {
359
+ ...(defaultReasoningEffort !== undefined ? { defaultReasoningEffort } : {}),
360
+ },
325
361
  };
326
362
  });
327
363
  const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
@@ -374,6 +410,34 @@ export async function listManagementModelRows(
374
410
  // the guard and the value) re-read the same file thousands of times on a large roster.
375
411
  const routedSlugs = publicModels.filter(m => m.provider !== "combo").map(m => routedSlug(m.provider, m.id));
376
412
  const catalogLadders = catalogModelEfforts([...new Set(routedSlugs)]);
413
+ // The gathered roster, keyed once by Codex-facing slug. A custom row looks itself up here —
414
+ // the gather materializes custom rows with the provider/native inheritance the catalog
415
+ // publishes, so this is the canonical resolved metadata the export must preserve, and no
416
+ // per-model fetch ever happens to obtain it.
417
+ const rosterByNamespaced = new Map(publicModels.map(model => [catalogModelSlug(model), model] as const));
418
+ const customRowsWithExportMetadata = visibleCustomModels.map(model => {
419
+ const catalogRow = rosterByNamespaced.get(model.namespaced);
420
+ // The gathered materialization's ladder is authoritative when it has one: it already
421
+ // folded the explicit override in and then applied the canonical bounds (the native
422
+ // clamp for a capability-backed id, learned refusals), so re-declaring the raw override
423
+ // here would undo a bound. Two raw values still win: the explicit empty ladder — "no
424
+ // rungs" is an operator statement no normalization may fill in, even against a stale
425
+ // gathered row — and any override with no gathered ladder behind it.
426
+ const declaredEfforts = Array.isArray(model.reasoningEfforts) && model.reasoningEfforts.length === 0
427
+ ? model.reasoningEfforts
428
+ : catalogRow?.reasoningEfforts !== undefined
429
+ ? catalogRow.reasoningEfforts
430
+ : model.reasoningEfforts;
431
+ return {
432
+ ...model,
433
+ exportMetadata: customRowExportMetadata(
434
+ config,
435
+ model,
436
+ catalogRow,
437
+ effectiveModelReasoningEfforts(config, model.provider, model.id, declaredEfforts, catalogLadders),
438
+ ),
439
+ };
440
+ });
377
441
  const dedupedRouted = publicModels.map((m): ManagementModelRow | null => {
378
442
  // Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
379
443
  const namespaced = catalogModelSlug(m);
@@ -395,7 +459,7 @@ export async function listManagementModelRows(
395
459
  ...m,
396
460
  ...displayName,
397
461
  namespaced,
398
- disabled: [...disabled].some(stored => (
462
+ disabled: antigravityFamilyDisabled(config, m, models) || [...disabled].some(stored => (
399
463
  (!nativeAlias && stored === namespaced) || slugEquals(stored, m.provider, m.id)
400
464
  )),
401
465
  ...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
@@ -407,13 +471,25 @@ export async function listManagementModelRows(
407
471
  ...(routed ? { reasoningOverridden: reasoningOverrideFor(config, m.provider, m.id, catalogLadders) } : {}),
408
472
  ...(inputModalitiesDeclared !== undefined ? { inputModalitiesDeclared } : {}),
409
473
  ...(contextWindowDeclared !== undefined ? { contextWindowDeclared } : {}),
474
+ // The authoritative export default is the GATHERED row's own declared default — never
475
+ // the picker value above, whose medium → low → first fallback invents a default no
476
+ // source declared. The object exists even when empty: its presence is what tells
477
+ // `toExportModel` and the CLI that an absent default means "no default", not "fall
478
+ // back to the row". A default outside the effective ladder is dropped for the same
479
+ // reason the settings route rejects one: the proxy would never send it.
480
+ exportMetadata: {
481
+ ...(m.defaultReasoningEffort !== undefined
482
+ && (reasoningEfforts === undefined || reasoningEfforts.includes(m.defaultReasoningEffort))
483
+ ? { defaultReasoningEffort: m.defaultReasoningEffort }
484
+ : {}),
485
+ },
410
486
  };
411
487
  }).filter((row): row is ManagementModelRow => row !== null);
412
488
  // Manual OpenAI rows retain their routed selector but replace the bare dashboard row.
413
489
  // Account-qualified rows remain distinct, explicitly selected routes.
414
490
  const visibleNative = native.filter(model => model.id.includes("/")
415
491
  || !customNamespaced.has(routedSlug(model.provider, model.id)));
416
- const rows = [...visibleNative, ...dedupedRouted, ...visibleCustomModels];
492
+ const rows = [...visibleNative, ...dedupedRouted, ...customRowsWithExportMetadata];
417
493
  // Include disabled rows and configured aliases before the export visibility filter:
418
494
  // a hidden real `x--fast` must never become a synthetic selector for another model.
419
495
  const knownIds = config.fastRows === false ? new Set<string>() : knownEffortRowIds(config);
@@ -422,6 +498,23 @@ export async function listManagementModelRows(
422
498
  const pending = initialModelSelectionPending(config.providers[row.provider]);
423
499
  const modelCosts = Object.hasOwn(config.providers, row.provider)
424
500
  ? config.providers[row.provider]?.modelCosts : undefined;
501
+ // Derived once here so the wire row carries the same booleans the export projection
502
+ // serializes; the CLI reads /api/models and cannot re-derive them without duplicating
503
+ // this policy. Custom rows read their effective view from `exportMetadata`; routed and
504
+ // native rows read their own evidence.
505
+ const supportsTools = row.exportMetadata?.supportsTools
506
+ ?? row.supportsTools
507
+ ?? knownToolsSupport({
508
+ capabilities: row.capabilities,
509
+ parallelToolCalls: row.parallelToolCalls,
510
+ native: row.native,
511
+ });
512
+ const supportsReasoning = row.exportMetadata?.supportsReasoning
513
+ ?? row.supportsReasoning
514
+ ?? knownReasoningSupport({
515
+ reasoningEfforts: row.reasoningEfforts,
516
+ supportsReasoningSummaries: row.supportsReasoningSummaries,
517
+ });
425
518
  return {
426
519
  ...row,
427
520
  ...(!row.native && modelCosts !== undefined && Object.hasOwn(modelCosts, row.id)
@@ -429,12 +522,52 @@ export async function listManagementModelRows(
429
522
  ...(pending ? { disabled: true, initialSelectionPending: true } : {}),
430
523
  fastRowAvailable: !row.disabled && !pending
431
524
  && !knownIds.has(fastRowId(row.namespaced)) && catalogFastRowEligible(config, row),
525
+ // An explicit false survives; derivation itself is true-or-nothing.
526
+ ...(typeof supportsTools === "boolean" ? { supportsTools } : {}),
527
+ ...(typeof supportsReasoning === "boolean" ? { supportsReasoning } : {}),
432
528
  };
433
529
  });
434
530
  }
435
531
 
436
- /** `/api/models` row → the narrower input the client-config serializers accept. */
532
+ /**
533
+ * `/api/models` row → the narrower input the client-config serializers accept.
534
+ *
535
+ * A row's `exportMetadata`, when present, is the authority: custom rows project through it
536
+ * because their own fields are stored overrides (an absent field means "not overridden"),
537
+ * and routed/native rows carry the declared default in it because the row-level
538
+ * `defaultReasoningEffort` is the picker's preference order, whose medium → low → first
539
+ * fallback fabricates a default no source declared.
540
+ */
437
541
  export function toExportModel(row: ManagementModelRow): ExportModel {
542
+ const effective = row.exportMetadata;
543
+ const contextWindow = effective?.contextWindow ?? row.contextWindow;
544
+ const maxTokens = effective?.maxTokens ?? row.maxOutputTokens;
545
+ const maxInputTokens = effective?.maxInputTokens ?? row.maxInputTokens;
546
+ const inputModalities = effective?.inputModalities ?? row.inputModalities;
547
+ const reasoningEfforts = effective?.reasoningEfforts ?? row.reasoningEfforts;
548
+ // The projection is the authority on the default. When it exists, an absent default means
549
+ // NO declared default — falling back to the row would reintroduce the picker's synthetic
550
+ // medium/low value, which is exactly what this projection exists to keep out of exports.
551
+ // Rows without a projection (native-era shapes, hand-built rows) keep the row's own value.
552
+ const defaultReasoningEffort = effective !== undefined
553
+ ? effective.defaultReasoningEffort
554
+ : row.defaultReasoningEffort;
555
+ const supportsReasoningSummaries = effective?.supportsReasoningSummaries ?? row.supportsReasoningSummaries;
556
+ // Projection first, then the row's own explicit boolean (an external producer may state
557
+ // false, which is evidence), and only then derivation from positive evidence.
558
+ const supportsTools = effective?.supportsTools
559
+ ?? row.supportsTools
560
+ ?? knownToolsSupport({
561
+ capabilities: row.capabilities,
562
+ parallelToolCalls: row.parallelToolCalls,
563
+ native: row.native,
564
+ });
565
+ const supportsReasoning = effective?.supportsReasoning
566
+ ?? row.supportsReasoning
567
+ ?? knownReasoningSupport({
568
+ reasoningEfforts: row.reasoningEfforts,
569
+ supportsReasoningSummaries: row.supportsReasoningSummaries,
570
+ });
438
571
  return {
439
572
  namespaced: row.namespaced,
440
573
  provider: row.provider,
@@ -442,11 +575,17 @@ export function toExportModel(row: ManagementModelRow): ExportModel {
442
575
  fastRowAvailable: row.fastRowAvailable === true,
443
576
  ...(row.native ? { native: true } : {}),
444
577
  ...(row.displayName && row.displayNameSource !== "fallback" ? { displayName: row.displayName } : {}),
445
- ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
446
- ...(row.maxOutputTokens !== undefined ? { maxTokens: row.maxOutputTokens } : {}),
447
- ...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
448
- ...(row.reasoningEfforts ? { reasoningEfforts: row.reasoningEfforts } : {}),
449
- ...(row.defaultReasoningEffort ? { defaultReasoningEffort: row.defaultReasoningEffort } : {}),
578
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
579
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
580
+ ...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
581
+ ...(inputModalities ? { inputModalities } : {}),
582
+ // An empty array is a declaration ("no rungs"), and `[]` is truthy: it survives here.
583
+ ...(reasoningEfforts ? { reasoningEfforts } : {}),
584
+ ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
585
+ // An explicit false is evidence and survives; only derivation is true-or-nothing.
586
+ ...(typeof supportsTools === "boolean" ? { supportsTools } : {}),
587
+ ...(typeof supportsReasoning === "boolean" ? { supportsReasoning } : {}),
588
+ ...(supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries } : {}),
450
589
  };
451
590
  }
452
591
 
@@ -51,7 +51,7 @@ import type { CodexNativeRestoreResult } from "../../codex/inject";
51
51
  import type { OcxConfig } from "../../types";
52
52
  import { jsonResponse } from "../auth-cors";
53
53
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
54
- import type { ManagementContext } from "./context";
54
+ import { managementInferencePort, type ManagementContext } from "./context";
55
55
 
56
56
  export type NativeIntegrationClientId = "claude" | "grok" | "codex" | "claude-desktop";
57
57
 
@@ -540,7 +540,7 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
540
540
  * a stale config.hostname picks the wrong loopback policy branch entirely.
541
541
  */
542
542
  const runtime = (deps.readRuntimePort ?? readRuntimePort)(process.pid);
543
- const port = runtime?.port ?? (Number(ctx.url.port) || config.port);
543
+ const port = deps.liveListenPort?.() ?? runtime?.port ?? managementInferencePort(ctx);
544
544
  const hostname = runtime?.hostname ?? config.hostname;
545
545
 
546
546
  /*
@@ -1,3 +1,5 @@
1
+ import { readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
2
+ import { ConfigWritePublishedError } from "../../config/persist-unlocked";
1
3
  import { parseAnthropicModelRoutes, readAnthropicModelRoutes } from "../../oauth/anthropic-model-routes";
2
4
  import { effectiveAnthropicAccountThreshold } from "../../oauth/anthropic-account-threshold";
3
5
  import { handleAnthropicAccountThreshold } from "./anthropic-account-threshold";
@@ -16,6 +18,7 @@ import {
16
18
  readConfigDiagnostics,
17
19
  reconcileLiveConfigFromDisk,
18
20
  saveConfigPreservingClaudeCode,
21
+ mutatePersistedConfig,
19
22
  } from "../../config";
20
23
  import {
21
24
  clearLoginState,
@@ -210,6 +213,44 @@ function genericOAuthProviderConfig(provider: string, config: ManagementContext[
210
213
  return definition?.resolveProviderConfig?.(config) ?? definition?.providerConfig;
211
214
  }
212
215
 
216
+ /** Both Anthropic writers share publication-aware recovery from the atomic mutation owner. */
217
+ function persistAnthropicPoolPatch(
218
+ config: OcxConfig,
219
+ patch: (target: OcxConfig) => { changed: boolean; value: NonNullable<OcxConfig["anthropicAccountPool"]> },
220
+ ): { status: "saved"; warning?: "config_bookkeeping_failed" } | { status: "failed"; response: Response } {
221
+ const before = readConfigFileSnapshot();
222
+ const unknown = () => ({ status: "failed" as const, response: jsonResponse({
223
+ error: "Pool settings save state is unknown; reload settings before editing again",
224
+ code: "config_save_state_unknown",
225
+ }, 409) });
226
+ try {
227
+ const saved = mutatePersistedConfig(patch);
228
+ if (saved.status === "unavailable") return unknown();
229
+ config.anthropicAccountPool = saved.value;
230
+ reconcileLiveStateStores();
231
+ return { status: "saved" };
232
+ } catch (error) {
233
+ const persisted = readConfigFileSnapshot();
234
+ if (persisted.diagnostics.source !== "file" || persisted.raw === undefined) return unknown();
235
+ // File reads may salvage hand edits; recovery success requires a strict, authoritative document.
236
+ let validated: ReturnType<typeof validateConfigCandidate>;
237
+ try { validated = validateConfigCandidate(JSON.parse(persisted.raw.replace(/^\uFEFF/, ""))); }
238
+ catch { return unknown(); }
239
+ if (!validated.ok) return unknown();
240
+ const current = validated.config;
241
+ const expected = patch(structuredClone(current)).value;
242
+ const matches = JSON.stringify(current.anthropicAccountPool) === JSON.stringify(expected);
243
+ if (matches) {
244
+ config.anthropicAccountPool = current.anthropicAccountPool;
245
+ // A failed reconcile must not turn confirmed publication back into a rollback.
246
+ try { reconcileLiveStateStores(); } catch { /* Fixed warning covers bookkeeping failure. */ }
247
+ return { status: "saved", warning: "config_bookkeeping_failed" };
248
+ }
249
+ if (error instanceof ConfigWritePublishedError || persisted.raw !== before.raw) return unknown();
250
+ return { status: "failed", response: jsonResponse({ error: "Pool settings could not be saved" }, 500) };
251
+ }
252
+ }
253
+
213
254
  export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<Response | null> {
214
255
  const { req, url, config, deps, principal, syncClaudeAgentDefsBestEffort } = ctx;
215
256
 
@@ -565,7 +606,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
565
606
  if (req.method !== "GET" && !isPlainRecord(rawBody)) {
566
607
  return jsonResponse({ error: "body must be an object" }, 400);
567
608
  }
568
- const fields = rawBody as { provider?: unknown; enabled?: unknown; strategy?: unknown; stickyLimit?: unknown; autoSwitchThreshold?: unknown; quotaWindow?: unknown; maxConcurrentPerAccount?: unknown; routes?: unknown };
609
+ const fields = rawBody as { provider?: unknown; enabled?: unknown; strategy?: unknown; stickyLimit?: unknown; autoSwitchThreshold?: unknown; quotaWindow?: unknown; maxConcurrentPerAccount?: unknown; routes?: unknown; nativeMessages?: unknown };
569
610
  const provider = req.method === "GET"
570
611
  ? (url.searchParams.get("provider") ?? "").trim().toLowerCase()
571
612
  : (typeof fields.provider === "string" ? fields.provider.trim().toLowerCase() : "");
@@ -573,6 +614,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
573
614
  if (!provider || !kind) {
574
615
  return jsonResponse({ error: "pool settings are only available for the codex, anthropic and generic OAuth pools" }, 400);
575
616
  }
617
+ if (Object.hasOwn(fields, "nativeMessages")) {
618
+ if (kind !== "anthropic") return jsonResponse({ error: "nativeMessages is only part of the anthropic pool contract" }, 400);
619
+ if (typeof fields.nativeMessages !== "boolean") return jsonResponse({ error: "nativeMessages must be a boolean" }, 400);
620
+ }
576
621
  // Validated by the SHARED parsers before any kind-specific write, so a bad strategy or
577
622
  // sticky limit is refused identically whichever pool is addressed.
578
623
  let strategy: string | undefined;
@@ -625,17 +670,24 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
625
670
  if (stickyLimit !== undefined) config.accountPoolStickyLimit = stickyLimit;
626
671
  if (autoSwitchThreshold !== undefined) config.autoSwitchThreshold = autoSwitchThreshold;
627
672
  } else if (kind === "anthropic") {
628
- const pool = { ...(config.anthropicAccountPool ?? {}) };
629
- if (fields.enabled !== undefined) pool.enabled = fields.enabled as boolean;
630
- if (strategy !== undefined) pool.strategy = strategy as never;
631
- if (stickyLimit !== undefined) pool.stickyLimit = stickyLimit;
632
- if (autoSwitchThreshold !== undefined) pool.autoSwitchThreshold = autoSwitchThreshold;
633
- if (quotaWindow !== undefined) pool.quotaWindow = quotaWindow as never;
634
- if (Object.hasOwn(fields, "routes")) {
635
- if (fields.routes === null) delete pool.routes;
636
- else if (parsedRoutes?.ok) pool.routes = parsedRoutes.routes;
637
- }
638
- config.anthropicAccountPool = pool;
673
+ const patchPool = (target: OcxConfig) => {
674
+ const pool = { ...(target.anthropicAccountPool ?? {}) };
675
+ if (fields.nativeMessages !== undefined) pool.nativeMessages = fields.nativeMessages as boolean;
676
+ if (fields.enabled !== undefined) pool.enabled = fields.enabled as boolean;
677
+ if (strategy !== undefined) pool.strategy = strategy as never;
678
+ if (stickyLimit !== undefined) pool.stickyLimit = stickyLimit;
679
+ if (autoSwitchThreshold !== undefined) pool.autoSwitchThreshold = autoSwitchThreshold;
680
+ if (quotaWindow !== undefined) pool.quotaWindow = quotaWindow as never;
681
+ if (Object.hasOwn(fields, "routes")) {
682
+ if (fields.routes === null) delete pool.routes;
683
+ else if (parsedRoutes?.ok) pool.routes = parsedRoutes.routes;
684
+ }
685
+ target.anthropicAccountPool = pool;
686
+ return { changed: true, value: pool };
687
+ };
688
+ const saved = persistAnthropicPoolPatch(config, patchPool);
689
+ if (saved.status === "failed") return saved.response;
690
+ return jsonResponse({ ...unifiedPoolSettingsDto(config, provider, kind), ...(saved.warning ? { warning: saved.warning } : {}) });
639
691
  } else {
640
692
  const prov = config.providers[provider]!;
641
693
  const next = { ...(prov.oauthAccountFailover ?? {}) };
@@ -679,6 +731,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
679
731
  stickyLimit: normalizeAccountPoolStickyLimit(pool.stickyLimit),
680
732
  quotaWindow: normalizeAccountPoolQuotaWindow(pool.quotaWindow),
681
733
  ...readAnthropicModelRoutes(pool.routes),
734
+ nativeMessages: !Object.hasOwn(pool, "nativeMessages") || pool.nativeMessages === true,
682
735
  experimental: true,
683
736
  });
684
737
  }
@@ -696,8 +749,13 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
696
749
  quotaWindow?: unknown;
697
750
  maxConcurrentPerAccount?: unknown;
698
751
  routes?: unknown;
752
+ nativeMessages?: unknown;
699
753
  };
700
754
  const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
755
+ if (Object.hasOwn(body, "nativeMessages")) {
756
+ if (provider !== "anthropic") return jsonResponse({ error: "nativeMessages is only part of the anthropic pool contract" }, 400);
757
+ if (typeof body.nativeMessages !== "boolean") return jsonResponse({ error: "nativeMessages must be a boolean" }, 400);
758
+ }
701
759
  if (provider !== "anthropic") {
702
760
  const {
703
761
  poolSettingsCapability, genericPoolSettingsDto, parseGenericPoolStrategy, parseGenericAutoSwitchThreshold,
@@ -796,28 +854,36 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
796
854
  const parsedLegacyRoutes = Object.hasOwn(body, "routes") && body.routes !== null
797
855
  ? parseAnthropicModelRoutes(body.routes) : null;
798
856
  if (parsedLegacyRoutes && !parsedLegacyRoutes.ok) return jsonResponse({ error: parsedLegacyRoutes.error }, 400);
799
- const routes = Object.hasOwn(body, "routes")
800
- ? (parsedLegacyRoutes?.ok ? parsedLegacyRoutes.routes : undefined)
801
- : config.anthropicAccountPool?.routes;
802
- config.anthropicAccountPool = {
803
- enabled,
804
- autoSwitchThreshold: threshold,
805
- ...(strategy !== undefined ? { strategy } : {}),
806
- ...(stickyLimit !== undefined ? { stickyLimit } : {}),
807
- ...(quotaWindow !== undefined ? { quotaWindow } : {}),
808
- ...(routes !== undefined ? { routes } : {}),
857
+ const legacyPatch = (target: OcxConfig) => {
858
+ const pool = { ...(target.anthropicAccountPool ?? {}) };
859
+ if (body.enabled !== undefined) pool.enabled = enabled;
860
+ else if (pool.enabled === undefined) pool.enabled = false;
861
+ if (body.nativeMessages !== undefined) pool.nativeMessages = body.nativeMessages as boolean;
862
+ if (body.autoSwitchThreshold !== undefined) pool.autoSwitchThreshold = threshold;
863
+ else if (pool.autoSwitchThreshold === undefined) pool.autoSwitchThreshold = 80;
864
+ if (body.strategy !== undefined) pool.strategy = strategy;
865
+ if (body.stickyLimit !== undefined) pool.stickyLimit = stickyLimit;
866
+ if (body.quotaWindow !== undefined) pool.quotaWindow = quotaWindow;
867
+ if (Object.hasOwn(body, "routes")) {
868
+ if (body.routes === null) delete pool.routes;
869
+ else if (parsedLegacyRoutes?.ok) pool.routes = parsedLegacyRoutes.routes;
870
+ }
871
+ target.anthropicAccountPool = pool;
872
+ return { changed: true, value: pool };
809
873
  };
810
- saveConfigPreservingClaudeCode(config);
811
- reconcileLiveStateStores();
874
+ const saved = persistAnthropicPoolPatch(config, legacyPatch);
875
+ if (saved.status === "failed") return saved.response;
812
876
  return jsonResponse({
813
877
  ok: true,
814
878
  provider,
815
- enabled,
816
- autoSwitchThreshold: threshold,
817
- strategy: normalizeAccountPoolStrategy(strategy),
818
- stickyLimit: normalizeAccountPoolStickyLimit(stickyLimit),
819
- quotaWindow: normalizeAccountPoolQuotaWindow(quotaWindow),
820
- routes: routes ?? null,
879
+ enabled: config.anthropicAccountPool?.enabled === true,
880
+ autoSwitchThreshold: config.anthropicAccountPool?.autoSwitchThreshold ?? 80,
881
+ strategy: normalizeAccountPoolStrategy(config.anthropicAccountPool?.strategy),
882
+ stickyLimit: normalizeAccountPoolStickyLimit(config.anthropicAccountPool?.stickyLimit),
883
+ quotaWindow: normalizeAccountPoolQuotaWindow(config.anthropicAccountPool?.quotaWindow),
884
+ routes: config.anthropicAccountPool?.routes ?? null,
885
+ nativeMessages: !Object.hasOwn(config.anthropicAccountPool ?? {}, "nativeMessages") || config.anthropicAccountPool?.nativeMessages === true,
886
+ ...(saved.warning ? { warning: saved.warning } : {}),
821
887
  experimental: true,
822
888
  });
823
889
  }