@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,170 @@
1
+ /** Explicit live lifecycle writes; the server remains the persistence authority. */
2
+ import { isValidProviderName } from "../config/provider-name";
3
+ import { providerRelativeSendPathConfigError } from "../config/provider-relative-send-path";
4
+ import { modelCapabilitiesConfigError, providerBaseUrlConfigError } from "../config/provider-validation";
5
+ import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-destination";
6
+ import { parseProviderEditorConfigDTO, providerManagementConfigError } from "../server/auth-cors";
7
+ import type { OcxProviderConfig } from "../types";
8
+ import { printProviderReceipt, runProviderAction } from "./provider-result";
9
+ import { serializeManagementJson } from "./json-input";
10
+ import {
11
+ CliUsageError, runtimeBaseUrl, runtimeRequest, takeFlag, takeOptionWithSyntax,
12
+ type RuntimeApiDeps,
13
+ } from "./runtime-api";
14
+
15
+ const USAGE = `Usage:
16
+ ocx provider add <name> --live [--adapter <id>] [--base-url <url>]
17
+ [--api-key <key>] [--api-key-transport <x-api-key|bearer>]
18
+ [--auth-mode <key|forward|oauth|local>] [--responses-path <path>]
19
+ [--default-model <id>] [--model <id> --text-only]
20
+ [--google-tool-schema-policy <compatible|reject-lossy>]
21
+ [--allow-private-network] [--set-default] [--force] [--json]
22
+ ocx provider set-default <name> --live [--json]
23
+ ocx provider remove <name> --live --yes [--json]
24
+ Add checks the target roster before writing; a concurrent change can still win the upsert race.
25
+ --force permits overwriting an observed provider. --sync is not supported with --live.
26
+ Remove uses server default reassignment, dependency checks and account/custom-model cleanup.`;
27
+ const AUTH_MODES = new Set(["key", "forward", "oauth", "local"]);
28
+ const OPTION_FIELDS = {
29
+ "--adapter": "adapter", "--base-url": "baseUrl", "--api-key": "apiKey",
30
+ "--api-key-transport": "apiKeyTransport", "--auth-mode": "authMode",
31
+ "--responses-path": "responsesPath", "--default-model": "defaultModel",
32
+ "--google-tool-schema-policy": "googleToolSchemaPolicy",
33
+ } as const;
34
+
35
+ function record(value: unknown): value is Record<string, unknown> {
36
+ return value !== null && typeof value === "object" && !Array.isArray(value);
37
+ }
38
+ function invalid(message: string): never { throw new CliUsageError(message, USAGE); }
39
+ function flag(args: string[], name: string): boolean {
40
+ if (args.filter(arg => arg === name).length > 1) invalid("An option was given more than once.");
41
+ return takeFlag(args, name);
42
+ }
43
+ function checkExplicitFields(fields: Record<string, unknown>): void {
44
+ if (typeof fields.baseUrl === "string" && providerBaseUrlConfigError(fields.baseUrl)) invalid("Invalid provider base URL.");
45
+ if (providerRelativeSendPathConfigError("responsesPath", fields.responsesPath)) invalid("Invalid responses path.");
46
+ if (fields.authMode !== undefined && !AUTH_MODES.has(String(fields.authMode))) invalid("Invalid auth mode.");
47
+ if (fields.apiKeyTransport !== undefined && !["x-api-key", "bearer"].includes(String(fields.apiKeyTransport))) invalid("Invalid API key transport.");
48
+ if (fields.googleToolSchemaPolicy !== undefined && !["compatible", "reject-lossy"].includes(String(fields.googleToolSchemaPolicy))) invalid("Invalid Google tool schema policy.");
49
+ }
50
+ function parseAdd(args: string[]) {
51
+ const force = flag(args, "--force");
52
+ const setDefault = flag(args, "--set-default");
53
+ const allowPrivateNetwork = flag(args, "--allow-private-network");
54
+ const textOnly = flag(args, "--text-only");
55
+ const model = takeOptionWithSyntax(args, "--model")?.value;
56
+ const fields: Record<string, unknown> = {};
57
+ for (const [option, field] of Object.entries(OPTION_FIELDS)) {
58
+ const value = takeOptionWithSyntax(args, option)?.value;
59
+ if (value !== undefined) {
60
+ if (!value.trim()) invalid("Provider options require nonempty values.");
61
+ fields[field] = value;
62
+ }
63
+ }
64
+ if (allowPrivateNetwork) fields.allowPrivateNetwork = true;
65
+ if (args.length) invalid("Unexpected provider argument or option.");
66
+ if (model !== undefined && !textOnly) invalid("--model requires --text-only.");
67
+ if (model !== undefined && modelCapabilitiesConfigError({ [model]: { inputModalities: ["text"] } })) invalid("Invalid model capability selection.");
68
+ checkExplicitFields(fields);
69
+ return { force, setDefault, fields, textOnly, model };
70
+ }
71
+
72
+ function presetSeed(name: string, payload: unknown): Record<string, unknown> | undefined {
73
+ if (!record(payload) || !Array.isArray(payload.providers)
74
+ || payload.providers.some(row => !record(row) || typeof row.id !== "string")) {
75
+ throw new Error("Invalid provider preset response.");
76
+ }
77
+ const matches = payload.providers.filter(row => row.id === name);
78
+ if (matches.length > 1) throw new Error("Ambiguous provider preset response.");
79
+ const preset = matches[0] as Record<string, unknown> | undefined;
80
+ if (!preset) return undefined;
81
+ // The target catalog's custom row is a form placeholder, not a provider seed.
82
+ if (name === "custom" && preset.baseUrl === "") return undefined;
83
+ if (name === "openai") {
84
+ const seed = preset.provider;
85
+ if (!record(seed) || typeof seed.baseUrl !== "string"
86
+ || !isCanonicalOpenAiForwardProvider(seed as unknown as OcxProviderConfig)
87
+ || !["pool", "direct"].includes(String(seed.codexAccountMode))
88
+ || Object.hasOwn(seed, "allowPrivateNetwork")
89
+ || !parseProviderEditorConfigDTO({ defaultProvider: name, providers: { [name]: seed } }).ok
90
+ || providerRelativeSendPathConfigError("responsesPath", seed.responsesPath)) {
91
+ throw new Error("Invalid canonical provider preset.");
92
+ }
93
+ // Use the selected target's seed verbatim, never a local registry fallback.
94
+ return structuredClone(seed);
95
+ }
96
+ if (typeof preset.adapter !== "string" || !preset.adapter.trim()
97
+ || typeof preset.baseUrl !== "string" || providerBaseUrlConfigError(preset.baseUrl)
98
+ || typeof preset.auth !== "string" || !AUTH_MODES.has(preset.auth)) {
99
+ throw new Error("Invalid provider preset.");
100
+ }
101
+ const seed: Record<string, unknown> = { adapter: preset.adapter, baseUrl: preset.baseUrl, authMode: preset.auth };
102
+ for (const field of ["responsesPath", "chatCompletionsPath", "defaultModel"] as const) {
103
+ if (preset[field] !== undefined) {
104
+ if (typeof preset[field] !== "string" || !preset[field].trim()) throw new Error("Invalid provider preset.");
105
+ seed[field] = preset[field];
106
+ }
107
+ }
108
+ return seed;
109
+ }
110
+
111
+ async function add(name: string, options: ReturnType<typeof parseAdd>, wantsJson: boolean, deps: RuntimeApiDeps): Promise<number> {
112
+ const roster = await runtimeRequest<unknown>("/api/providers", { redirect: "error" }, deps);
113
+ if (!Array.isArray(roster) || roster.some(row => !record(row) || typeof row.name !== "string" || !isValidProviderName(row.name))) {
114
+ throw new Error("Invalid provider roster response.");
115
+ }
116
+ if (!options.force && roster.some(row => row.name === name)) {
117
+ console.error("Provider already exists on this target. Use --force to permit overwrite.");
118
+ return 5;
119
+ }
120
+ const presets = await runtimeRequest("/api/provider-presets", { redirect: "error" }, deps);
121
+ const seed = presetSeed(name, presets);
122
+ if (!seed && (name === "openai" || !options.fields.adapter || !options.fields.baseUrl)) {
123
+ throw new Error("Target preset unavailable. Custom providers require --adapter and --base-url.");
124
+ }
125
+ const provider = { ...seed, ...options.fields };
126
+ if (provider.googleToolSchemaPolicy !== undefined && provider.adapter !== "google") {
127
+ invalid("--google-tool-schema-policy requires the google adapter.");
128
+ }
129
+ if (provider.apiKey !== undefined && provider.authMode !== undefined && provider.authMode !== "key") {
130
+ invalid("--api-key requires key authentication; use the provider login workflow for account authentication.");
131
+ }
132
+ if (options.textOnly) {
133
+ const model = options.model ?? provider.defaultModel;
134
+ if (typeof model !== "string") invalid("--text-only requires --model or a default model.");
135
+ const capabilities = { [model]: { inputModalities: ["text"] } };
136
+ if (modelCapabilitiesConfigError(capabilities)) invalid("Invalid model capability selection.");
137
+ provider.modelCapabilities = capabilities;
138
+ }
139
+ if (name !== "openai" && providerManagementConfigError(name, provider)) invalid("Invalid provider configuration.");
140
+ const result = await runtimeRequest("/api/providers", {
141
+ method: "POST", redirect: "error",
142
+ body: serializeManagementJson({ name, provider, ...(options.setDefault ? { setDefault: true } : {}) }),
143
+ }, deps);
144
+ return printProviderReceipt(result, wantsJson, "added");
145
+ }
146
+
147
+ export async function handleProviderLifecycleRuntimeCommand(
148
+ sub: "add" | "remove" | "set-default", argv: string[], deps: RuntimeApiDeps = {},
149
+ ): Promise<number> {
150
+ return runProviderAction(async () => {
151
+ const args = [...argv];
152
+ const wantsJson = flag(args, "--json");
153
+ const name = args.shift();
154
+ if (!name || !isValidProviderName(name)) invalid("A valid provider name is required.");
155
+ const options = sub === "add" ? parseAdd(args) : undefined;
156
+ const yes = sub === "remove" ? flag(args, "--yes") : false;
157
+ if (args.length) invalid("Unexpected provider argument or option.");
158
+ if (sub === "remove" && !yes) invalid("Live removal requires --yes.");
159
+ if (name === "openai" && options && (Object.keys(options.fields).length || options.textOnly)) {
160
+ invalid("Canonical OpenAI uses the target preset unchanged; add flags cannot override its seed.");
161
+ }
162
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
163
+ if (options) return add(name, options, wantsJson, pinned);
164
+ const result = await runtimeRequest(`/api/providers?name=${encodeURIComponent(name)}`, {
165
+ method: sub === "remove" ? "DELETE" : "PATCH", redirect: "error",
166
+ ...(sub === "set-default" ? { body: JSON.stringify({ setDefault: true }) } : {}),
167
+ }, pinned);
168
+ return printProviderReceipt(result, wantsJson, sub === "remove" ? "removed" : "set-default");
169
+ });
170
+ }
@@ -0,0 +1,124 @@
1
+ import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
2
+ import { CliUsageError, RuntimeApiError, printData, terminalSafeText } from "./runtime-api";
3
+
4
+ const ERRORS: Readonly<Record<string, string>> = {
5
+ stale_provider_editor_baseline: "Provider baseline is stale. Read a fresh snapshot and review the changes before applying again.",
6
+ provider_config_conflict: "Provider configuration changed before the write. Inspect the target before retrying.",
7
+ provider_config_unavailable: "Provider configuration is unavailable on the target.",
8
+ invalid_provider_editor_body: "Expected a provider editor document containing only defaultProvider and providers.",
9
+ invalid_provider_editor_field: "The provider editor document contains a non-editable field.",
10
+ invalid_provider_editor_config: "The proposed provider configuration is invalid.",
11
+ invalid_provider_destination: "The target refused a provider destination.",
12
+ invalid_provider: "The target refused the provider configuration.",
13
+ invalid_provider_name: "The provider name is invalid.",
14
+ invalid_default_provider: "The default must name a configured provider.",
15
+ default_provider_disabled: "A disabled provider cannot be the default.",
16
+ provider_namespace_conflict: "The provider conflicts with an account namespace.",
17
+ provider_has_dependent_combos: "A routing combo still depends on this provider. Inspect its dependencies before removal.",
18
+ last_provider: "The last provider cannot be removed.",
19
+ };
20
+
21
+ function record(value: unknown): value is Record<string, unknown> {
22
+ return value !== null && typeof value === "object" && !Array.isArray(value)
23
+ && [Object.prototype, null].includes(Object.getPrototypeOf(value));
24
+ }
25
+
26
+ /** Read only own data values; never invoke response getters or stringify a body. */
27
+ function own(value: unknown, key: string): unknown {
28
+ if (!record(value)) return undefined;
29
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
30
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
31
+ }
32
+
33
+ /** Provider commands return numeric partial outcomes rather than losing them in runCliAction. */
34
+ export async function runProviderAction(action: () => Promise<number | void>): Promise<number> {
35
+ try {
36
+ return (await action()) ?? 0;
37
+ } catch (error) {
38
+ if (error instanceof CliUsageError) {
39
+ // Domain/input parsers supply static diagnostics; no raw parser/OS error reaches here.
40
+ console.error(`Error: ${terminalSafeText(error.message)}`);
41
+ if (error.usage) for (const line of error.usage.split("\n")) console.error(terminalSafeText(line));
42
+ return 2;
43
+ }
44
+ if (error instanceof RuntimeApiError) {
45
+ const code = own(error.body, "code") ?? own(own(error.body, "error"), "code");
46
+ const known = typeof code === "string" && Object.hasOwn(ERRORS, code) ? ERRORS[code] : undefined;
47
+ const fallback = error.status === 404 ? "Provider or management operation was not found on the selected target."
48
+ : error.status === 409 ? "The target refused a conflicting provider change. Read back before trying again."
49
+ : error.status === 503 ? "Management API is unavailable. Start the intended proxy; connected clients must make provider changes on their Hub. The write outcome may be unknown."
50
+ : "The provider request failed. Inspect the selected target before retrying; the write outcome may be unknown.";
51
+ console.error(`Error: ${known ?? fallback}`);
52
+ return error.status === 404 ? 4 : error.status === 409 ? 5 : 1;
53
+ }
54
+ console.error("Error: Provider operation did not return a usable outcome. Inspect the selected target before retrying; no rollback is implied.");
55
+ return 1;
56
+ }
57
+ }
58
+
59
+ const RECEIPT_FIELDS = new Set([
60
+ "success", "name", "defaultProvider", "droppedCustomModels", "disabled", "hasApiKey",
61
+ "xaiResponsesOptInState", "dependentShadowIntercept", "catalogRefresh",
62
+ ]);
63
+
64
+ function invalidReceipt(): never { throw new Error("Invalid provider receipt"); }
65
+ function safeLabel(value: unknown): value is string { return typeof value === "string" && value.length > 0 && value.length <= 4096; }
66
+
67
+ /** Copy the public receipt vocabulary. Unexpected bodies never become raw CLI output. */
68
+ export function printProviderReceipt(value: unknown, wantsJson: boolean, action: string): number {
69
+ if (!record(value) || own(value, "success") !== true
70
+ || Object.keys(value).some(key => !RECEIPT_FIELDS.has(key))) invalidReceipt();
71
+ const result: Record<string, unknown> = { success: true };
72
+ for (const key of ["name", "defaultProvider"] as const) {
73
+ if (!Object.hasOwn(value, key)) continue;
74
+ const label = own(value, key);
75
+ if (!safeLabel(label)) invalidReceipt();
76
+ result[key] = label;
77
+ }
78
+ for (const key of ["disabled", "hasApiKey"] as const) {
79
+ if (!Object.hasOwn(value, key)) continue;
80
+ const flag = own(value, key);
81
+ if (typeof flag !== "boolean") invalidReceipt();
82
+ result[key] = flag;
83
+ }
84
+ if (Object.hasOwn(value, "xaiResponsesOptInState")) {
85
+ const state = own(value, "xaiResponsesOptInState");
86
+ if (typeof state !== "boolean" && state !== "mixed") invalidReceipt();
87
+ result.xaiResponsesOptInState = state;
88
+ }
89
+ if (Object.hasOwn(value, "droppedCustomModels")) {
90
+ const count = own(value, "droppedCustomModels");
91
+ if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) invalidReceipt();
92
+ result.droppedCustomModels = count;
93
+ }
94
+ if (Object.hasOwn(value, "dependentShadowIntercept")) {
95
+ const shadow = own(value, "dependentShadowIntercept");
96
+ const model = own(shadow, "model"), enabled = own(shadow, "enabled");
97
+ if (!record(shadow) || !safeLabel(model) || typeof enabled !== "boolean"
98
+ || Object.keys(shadow).some(key => key !== "model" && key !== "enabled")) invalidReceipt();
99
+ result.dependentShadowIntercept = { model, enabled };
100
+ }
101
+ let pending = false;
102
+ let outcome = "No client catalog refresh was requested by this operation.";
103
+ if (Object.hasOwn(value, "catalogRefresh")) {
104
+ const raw = own(value, "catalogRefresh");
105
+ const refresh = raw === null ? null : normalizeCatalogDisposition(raw);
106
+ if (raw !== null && refresh === null) invalidReceipt();
107
+ result.catalogRefresh = refresh;
108
+ if (refresh?.status === "committed") {
109
+ outcome = refresh.degraded ? "Catalog committed with degraded provider observations." : "Catalog committed.";
110
+ } else if (refresh && refresh.reason !== "not-requested") {
111
+ pending = true;
112
+ outcome = `Configuration saved, but catalog did not converge (${refresh.reason}). Read back before retrying.`;
113
+ }
114
+ }
115
+ const lines = [`${action}: provider configuration saved.`, outcome];
116
+ if (result.defaultProvider) lines.push(`Default provider: ${result.defaultProvider}`);
117
+ if (result.droppedCustomModels !== undefined) lines.push(`Removed custom models: ${result.droppedCustomModels}`);
118
+ if (result.dependentShadowIntercept) {
119
+ const shadow = result.dependentShadowIntercept as { model: string; enabled: boolean };
120
+ lines.push(`Shadow-call dependency: ${shadow.model} (enabled: ${shadow.enabled}). Inspect its target.`);
121
+ }
122
+ printData(result, wantsJson, lines);
123
+ return pending ? 1 : 0;
124
+ }
@@ -12,6 +12,8 @@ import {
12
12
  takeOption,
13
13
  type RuntimeApiDeps,
14
14
  } from "./runtime-api";
15
+ import { takeProviderEditSettings } from "./provider-settings";
16
+ import { printProviderReceipt, runProviderAction } from "./provider-result";
15
17
  import { providerQuotaLine } from "./account-extended";
16
18
  import { pinSponsorRows } from "../providers/sponsor-order";
17
19
  import type { ProviderQuotaReportDto } from "./account-api";
@@ -42,7 +44,7 @@ const USAGE = `Usage:
42
44
  [--api-key-transport <x-api-key|bearer|->]
43
45
  [--headers <json>] [--enabled <on|off>] [--live-models <on|off>]
44
46
  [--retain-models <id,id|->] [--model <id> --text-only]
45
- [--xai-chat <on|off>]
47
+ [--xai-chat <on|off>] [--upstream-http-version <http1.1|->] [--fast <on|off>] [--context-window <tokens|->]
46
48
  [--allow-private-network <on|off>] [--model-context-tier <model=default|long_context>] [--json]
47
49
  ocx provider test <name> [--json]
48
50
  ocx provider quota [--refresh] [--json]
@@ -56,12 +58,12 @@ function cleared(value: string | undefined): string | undefined {
56
58
  return value === "-" ? "" : value;
57
59
  }
58
60
 
59
- async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
61
+ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<number> {
60
62
  const args = [...argv];
61
63
  const name = args.shift()?.trim();
62
64
  if (!name) throw new CliUsageError("provider name is required", USAGE);
63
65
  const wantsJson = takeFlag(args, "--json");
64
- const patch: Record<string, unknown> = {};
66
+ const patch: Record<string, unknown> = takeProviderEditSettings(args);
65
67
  const adapter = takeOption(args, "--adapter");
66
68
  const baseUrl = takeOption(args, "--base-url");
67
69
  const defaultModel = cleared(takeOption(args, "--default-model"));
@@ -139,13 +141,13 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
139
141
  if (allowPrivateNetwork !== undefined) patch.allowPrivateNetwork = allowPrivateNetwork;
140
142
  if (Object.keys(patch).length === 0) throw new CliUsageError("at least one edit option is required", USAGE);
141
143
  const result = await runtimeRequest(`/api/providers?name=${encodeURIComponent(name)}`, {
142
- method: "PATCH",
144
+ method: "PATCH", redirect: "error",
143
145
  body: JSON.stringify(patch),
144
146
  }, deps);
145
- printData(result, wantsJson, [`Updated provider ${name}.`]);
147
+ return printProviderReceipt(result, wantsJson, "Provider edit");
146
148
  }
147
149
 
148
- async function testProvider(argv: string[], deps: RuntimeApiDeps): Promise<void> {
150
+ async function testProvider(argv: string[], deps: RuntimeApiDeps): Promise<number> {
149
151
  const args = [...argv];
150
152
  const name = args.shift()?.trim();
151
153
  const wantsJson = takeFlag(args, "--json");
@@ -159,7 +161,7 @@ async function testProvider(argv: string[], deps: RuntimeApiDeps): Promise<void>
159
161
  `${name}: not applicable`,
160
162
  "Static catalog; no live model-discovery endpoint to test.",
161
163
  ]);
162
- return;
164
+ return 0;
163
165
  }
164
166
  const ok = result.ok === true;
165
167
  printData(result, wantsJson, [
@@ -167,7 +169,7 @@ async function testProvider(argv: string[], deps: RuntimeApiDeps): Promise<void>
167
169
  String(result.message ?? result.error ?? "No detail"),
168
170
  `Latency: ${String(result.latencyMs ?? "?")} ms`,
169
171
  ]);
170
- if (!ok) process.exitCode = 1;
172
+ return ok ? 0 : 1;
171
173
  }
172
174
 
173
175
  async function quota(argv: string[], deps: RuntimeApiDeps): Promise<void> {
@@ -310,9 +312,8 @@ async function keychain(argv: string[], deps: RuntimeApiDeps): Promise<void> {
310
312
  }
311
313
 
312
314
  export async function handleProviderRuntimeCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise<number | null> {
313
- const handlers: Record<string, (args: string[], deps: RuntimeApiDeps) => Promise<void>> = {
314
- edit,
315
- update: edit,
315
+ if (sub === "edit" || sub === "update") return runProviderAction(() => edit(argv, deps));
316
+ const handlers: Record<string, (args: string[], deps: RuntimeApiDeps) => Promise<number | void>> = {
316
317
  test: testProvider,
317
318
  quota,
318
319
  resets,
@@ -323,7 +324,9 @@ export async function handleProviderRuntimeCommand(sub: string, argv: string[],
323
324
  };
324
325
  const handler = handlers[sub];
325
326
  if (!handler) return null;
326
- return runCliAction(() => handler(argv, deps));
327
+ let outcome: number | void = 0;
328
+ const exit = await runCliAction(async () => { outcome = await handler(argv, deps); });
329
+ return exit || outcome || 0;
327
330
  }
328
331
 
329
332
  export const PROVIDER_RUNTIME_USAGE = USAGE;
@@ -0,0 +1,143 @@
1
+ import { isValidProviderName } from "../config/provider-name";
2
+ import { requestPacingConfigError } from "../config/schema/leaf-validators";
3
+ import { readJsonInput, serializeManagementJson } from "./json-input";
4
+ import { printProviderReceipt, runProviderAction } from "./provider-result";
5
+ import {
6
+ CliUsageError, RuntimeApiError, printData, runtimeBaseUrl, runtimeRequest,
7
+ takeFlag, takeOptionWithSyntax, type RuntimeApiDeps,
8
+ } from "./runtime-api";
9
+
10
+ const PACING_USAGE = "Usage: ocx provider pacing <name> [--file <FILE|-> | --enabled <on|off> --rpm <number> --min-interval-ms <integer> --max-concurrent <integer>] [--json]";
11
+
12
+ function onOff(value: string, flag: string): boolean {
13
+ if (value !== "on" && value !== "off") throw new CliUsageError(`${flag} must be on or off`);
14
+ return value === "on";
15
+ }
16
+
17
+ function positiveInteger(value: string, flag: string): number {
18
+ const number = Number(value);
19
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(number) || number <= 0) {
20
+ throw new CliUsageError(`${flag} must be a positive safe integer`);
21
+ }
22
+ return number;
23
+ }
24
+
25
+ /** Consume only the additional edit options; the existing edit parser owns all others. */
26
+ export function takeProviderEditSettings(args: string[]): Record<string, unknown> {
27
+ const patch: Record<string, unknown> = {};
28
+ const version = takeOptionWithSyntax(args, "--upstream-http-version")?.value;
29
+ const fast = takeOptionWithSyntax(args, "--fast")?.value;
30
+ const context = takeOptionWithSyntax(args, "--context-window")?.value;
31
+ if (version !== undefined) {
32
+ if (version !== "http1.1" && version !== "-") {
33
+ throw new CliUsageError("--upstream-http-version must be http1.1 or -");
34
+ }
35
+ patch.upstreamHttpVersion = version === "-" ? null : version;
36
+ }
37
+ if (fast !== undefined) patch.fastEnabled = onOff(fast, "--fast");
38
+ if (context !== undefined) patch.contextWindow = context === "-" ? null : positiveInteger(context, "--context-window");
39
+ return patch;
40
+ }
41
+
42
+ function record(value: unknown): value is Record<string, unknown> {
43
+ return value !== null && typeof value === "object" && !Array.isArray(value);
44
+ }
45
+
46
+ function validRules(value: unknown): value is Record<string, unknown> {
47
+ return record(value) && requestPacingConfigError(value) === null;
48
+ }
49
+
50
+ function takePacingScalars(args: string[]): Record<string, unknown> {
51
+ const patch: Record<string, unknown> = {};
52
+ const enabled = takeOptionWithSyntax(args, "--enabled")?.value;
53
+ const rpm = takeOptionWithSyntax(args, "--rpm")?.value;
54
+ const interval = takeOptionWithSyntax(args, "--min-interval-ms")?.value;
55
+ const concurrent = takeOptionWithSyntax(args, "--max-concurrent")?.value;
56
+ if (enabled !== undefined) patch.enabled = onOff(enabled, "--enabled");
57
+ if (rpm !== undefined) {
58
+ const number = Number(rpm);
59
+ if (!rpm.trim() || !Number.isFinite(number)) throw new CliUsageError("--rpm must be a finite number");
60
+ patch.requestsPerMinute = number;
61
+ }
62
+ if (interval !== undefined) patch.minIntervalMs = positiveInteger(interval, "--min-interval-ms");
63
+ if (concurrent !== undefined) patch.maxConcurrentRequests = positiveInteger(concurrent, "--max-concurrent");
64
+ // Validate numeric bounds before target resolution; enabling alone may use stored rules.
65
+ if (!validRules({ ...patch, enabled: false })) throw new CliUsageError("Invalid pacing limits", PACING_USAGE);
66
+ return patch;
67
+ }
68
+
69
+ async function configuredRules(name: string, deps: RuntimeApiDeps): Promise<Record<string, unknown> | null> {
70
+ const config = await runtimeRequest("/api/config", { redirect: "error" }, deps);
71
+ if (!record(config) || !record(config.providers)) throw new Error("Invalid provider configuration response");
72
+ if (!Object.hasOwn(config.providers, name)) throw new RuntimeApiError("Provider not found", 404, null);
73
+ const provider = config.providers[name];
74
+ if (!record(provider)) throw new Error("Invalid provider configuration response");
75
+ if (!Object.hasOwn(provider, "requestPacing")) return null;
76
+ if (!validRules(provider.requestPacing)) throw new Error("Invalid stored pacing rules");
77
+ return provider.requestPacing;
78
+ }
79
+
80
+ function pacingStatus(value: unknown, name: string): Record<string, unknown> {
81
+ const keys = ["provider", "enabled", "queued", "nextSlotInMs", "inFlight", "lastStartedAt", "lastModelId"];
82
+ if (!record(value) || Object.keys(value).some(key => !keys.includes(key))
83
+ || value.provider !== name || typeof value.enabled !== "boolean") {
84
+ throw new Error("Invalid pacing status response");
85
+ }
86
+ for (const key of ["queued", "nextSlotInMs", "inFlight", "lastStartedAt"]) {
87
+ if ((key === "inFlight" || key === "lastStartedAt") && !Object.hasOwn(value, key)) continue;
88
+ const number = value[key];
89
+ if (typeof number !== "number" || !Number.isFinite(number) || number < 0
90
+ || (key !== "lastStartedAt" && !Number.isSafeInteger(number))) {
91
+ throw new Error("Invalid pacing status response");
92
+ }
93
+ }
94
+ if (Object.hasOwn(value, "lastModelId") && typeof value.lastModelId !== "string") {
95
+ throw new Error("Invalid pacing status response");
96
+ }
97
+ return value;
98
+ }
99
+
100
+ export async function handleProviderPacingCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
101
+ return runProviderAction(async () => {
102
+ const args = [...argv];
103
+ const name = args.shift()?.trim();
104
+ if (!name || !isValidProviderName(name)) throw new CliUsageError("A valid provider name is required", PACING_USAGE);
105
+ const wantsJson = takeFlag(args, "--json");
106
+ const file = takeOptionWithSyntax(args, "--file")?.value;
107
+ const scalars = takePacingScalars(args);
108
+ if (args.length) throw new CliUsageError("Unexpected pacing argument(s)", PACING_USAGE);
109
+ const editing = Object.keys(scalars).length > 0;
110
+ if (file !== undefined && editing) throw new CliUsageError("--file cannot be combined with pacing scalar options", PACING_USAGE);
111
+ let fileBody: string | undefined;
112
+ if (file !== undefined) {
113
+ const rules = await readJsonInput(file, deps, "Pacing JSON input");
114
+ if (!validRules(rules)) throw new CliUsageError("Invalid pacing rules", PACING_USAGE);
115
+ fileBody = serializeManagementJson({ requestPacing: rules });
116
+ }
117
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
118
+ if (fileBody !== undefined || editing) {
119
+ let body = fileBody;
120
+ if (body === undefined) {
121
+ const observed = await configuredRules(name, pinned);
122
+ const rules = { ...(observed ?? { enabled: false }), ...scalars };
123
+ if (!validRules(rules)) throw new CliUsageError("Invalid resulting pacing rules", PACING_USAGE);
124
+ // PATCH replaces the observed block, including its model rules. This is not CAS.
125
+ body = serializeManagementJson({ requestPacing: rules });
126
+ }
127
+ const result = await runtimeRequest(`/api/providers?name=${encodeURIComponent(name)}`, {
128
+ method: "PATCH", redirect: "error", body,
129
+ }, pinned);
130
+ return printProviderReceipt(result, wantsJson, "Updated provider pacing");
131
+ }
132
+ const rules = await configuredRules(name, pinned);
133
+ const status = pacingStatus(await runtimeRequest(`/api/provider-request-pacing?name=${encodeURIComponent(name)}`, {
134
+ redirect: "error",
135
+ }, pinned), name);
136
+ printData({ provider: name, rules, status }, wantsJson, [
137
+ `${name}: ${rules === null ? "no pacing rules configured" : rules.enabled ? "pacing rules enabled" : "pacing rules disabled"}`,
138
+ ...(rules === null ? [] : [JSON.stringify(rules)]),
139
+ `Runtime: ${status.enabled ? "enabled" : "disabled"}; queued: ${status.queued}; next slot: ${status.nextSlotInMs} ms`,
140
+ ]);
141
+ return 0;
142
+ });
143
+ }