@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,142 @@
1
+ /** Bounded, serial observation of fixed management read surfaces. */
2
+ import { readBoundedResponseBytes } from "../lib/bounded-body";
3
+ import { runningProxyUpdateHeaders } from "../oauth/login-cli";
4
+ import { findLiveProxy, type LiveProxy } from "../server/proxy-liveness";
5
+ import { runtimeBaseUrl, type RuntimeApiDeps } from "./runtime-api";
6
+
7
+ const POLL_MS = 1_000;
8
+ const REQUEST_MS = 10_000;
9
+ const RESPONSE_BYTES = 32 * 1024 * 1024;
10
+ type ObservationPath = "/api/logs" | "/api/debug/injection-logs" | "/api/companion/settings" | "/api/usage";
11
+
12
+ export interface ObserveStreamDeps extends RuntimeApiDeps {
13
+ signal?: AbortSignal;
14
+ /** Reduced timings for isolated tests only; never command-line options. */
15
+ pollIntervalMs?: number;
16
+ requestTimeoutMs?: number;
17
+ }
18
+
19
+ export class ObservationError extends Error {}
20
+
21
+ function reduced(value: number | undefined, ceiling: number): number {
22
+ return value !== undefined && Number.isFinite(value) && value > 0 && value <= ceiling ? value : ceiling;
23
+ }
24
+
25
+ function interrupted<T>(work: Promise<T>, signal: AbortSignal): Promise<T> {
26
+ return new Promise((resolve, reject) => {
27
+ const abort = () => { cleanup(); reject(signal.reason); };
28
+ const cleanup = () => signal.removeEventListener("abort", abort);
29
+ signal.addEventListener("abort", abort, { once: true });
30
+ work.then(value => { cleanup(); resolve(value); }, error => { cleanup(); reject(error); });
31
+ if (signal.aborted) abort();
32
+ });
33
+ }
34
+
35
+ function waitForPoll(ms: number, signal: AbortSignal): Promise<void> {
36
+ return new Promise((resolve, reject) => {
37
+ const cleanup = () => { clearTimeout(timer); signal.removeEventListener("abort", abort); };
38
+ const abort = () => { cleanup(); reject(signal.reason); };
39
+ const timer = setTimeout(() => { cleanup(); resolve(); }, ms);
40
+ signal.addEventListener("abort", abort, { once: true });
41
+ if (signal.aborted) abort();
42
+ });
43
+ }
44
+
45
+ function identity(live: LiveProxy | undefined): string {
46
+ return JSON.stringify(live ? [live.pid, live.port, live.hostname, live.role] : null);
47
+ }
48
+
49
+ export interface ObserveStream {
50
+ signal: AbortSignal;
51
+ get(path: ObservationPath, query: URLSearchParams): Promise<unknown>;
52
+ wait(): Promise<void>;
53
+ }
54
+
55
+ export interface ObservationContext {
56
+ kind: "follow" | "snapshot";
57
+ limitOption?: "--limit" | "--scan-limit";
58
+ }
59
+
60
+ /** Each invocation owns its signals, in-flight request and pinned runtime identity. */
61
+ export async function withObserveStream(
62
+ deps: ObserveStreamDeps,
63
+ run: (stream: ObserveStream) => Promise<void | number>,
64
+ context: ObservationContext = { kind: "follow", limitOption: "--limit" },
65
+ ): Promise<number> {
66
+ const retry = context.kind === "follow" ? "restart follow" : "retry the command";
67
+ const controller = new AbortController();
68
+ let exit = 0;
69
+ const stop = (code: number) => {
70
+ if (controller.signal.aborted) return;
71
+ exit = code;
72
+ controller.abort();
73
+ };
74
+ const onInt = () => stop(130);
75
+ const onTerm = () => stop(143);
76
+ process.on("SIGINT", onInt);
77
+ process.on("SIGTERM", onTerm);
78
+ deps.signal?.addEventListener("abort", onInt, { once: true });
79
+ if (deps.signal?.aborted) onInt();
80
+ let pinned: { origin: string; identity: string } | undefined;
81
+ const signal = controller.signal;
82
+ const timeoutMs = reduced(deps.requestTimeoutMs, REQUEST_MS);
83
+ const get = async (path: ObservationPath, query: URLSearchParams): Promise<unknown> => {
84
+ signal.throwIfAborted();
85
+ let live: LiveProxy | undefined;
86
+ const origin = await interrupted(runtimeBaseUrl({ ...deps, findLiveProxy: async () => {
87
+ const found = await (deps.findLiveProxy ?? findLiveProxy)();
88
+ live = found ? { ...found } : undefined;
89
+ // Snapshot recovery comes from our observed target, never arbitrary transport text.
90
+ if (context.kind === "snapshot") {
91
+ if (!found) throw new ObservationError("Proxy is not running. Start it with: ocx start");
92
+ if (found.role === "client") throw new ObservationError("This listener runs in the client role and has no management API. Run this command on its serving hub, or open the hub dashboard.");
93
+ }
94
+ return found;
95
+ } }), signal);
96
+ signal.throwIfAborted();
97
+ const current = { origin, identity: identity(live) };
98
+ if (pinned && (pinned.origin !== current.origin || pinned.identity !== current.identity)) {
99
+ throw new ObservationError(`The runtime changed. ${context.kind === "follow" ? "Restart follow" : "Retry the command"} to observe the current runtime.`);
100
+ }
101
+ pinned = current;
102
+ const request = new AbortController();
103
+ const abort = () => request.abort();
104
+ signal.addEventListener("abort", abort, { once: true });
105
+ const timer = setTimeout(abort, timeoutMs);
106
+ try {
107
+ signal.throwIfAborted();
108
+ const responsePromise = (deps.fetchImpl ?? fetch)(`${origin}${path}?${query}`, {
109
+ method: "GET", headers: runningProxyUpdateHeaders(), credentials: "omit", redirect: "error", signal: request.signal,
110
+ });
111
+ // A test transport or late fetch may settle after abort. Release its body too.
112
+ void responsePromise.then(response => {
113
+ if (request.signal.aborted) void response.body?.cancel().catch(() => {});
114
+ }, () => {});
115
+ const response = await interrupted(responsePromise, request.signal);
116
+ const result = await readBoundedResponseBytes(response, { maxBytes: RESPONSE_BYTES, signal: request.signal });
117
+ signal.throwIfAborted();
118
+ request.signal.throwIfAborted();
119
+ if (result.oversized) throw new ObservationError(`The observation window exceeds 32 MiB. ${context.limitOption ? `Reduce ${context.limitOption} and ${retry}` : "Check the runtime response size and retry the command"}.`);
120
+ if (!response.ok) throw new ObservationError(`The observation request was refused. Check runtime access and ${retry}.`);
121
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(result.bytes));
122
+ } finally {
123
+ clearTimeout(timer);
124
+ signal.removeEventListener("abort", abort);
125
+ request.abort();
126
+ }
127
+ };
128
+ try {
129
+ signal.throwIfAborted();
130
+ const result = await run({ signal, get, wait: () => waitForPoll(reduced(deps.pollIntervalMs, POLL_MS), signal) });
131
+ return exit || result || 0;
132
+ } catch (error) {
133
+ if (exit) return exit;
134
+ console.error(`Error: ${error instanceof ObservationError ? error.message : `Observation failed or timed out. Check the runtime and ${retry}.`}`);
135
+ return 1;
136
+ } finally {
137
+ controller.abort();
138
+ process.removeListener("SIGINT", onInt);
139
+ process.removeListener("SIGTERM", onTerm);
140
+ deps.signal?.removeEventListener("abort", onInt);
141
+ }
142
+ }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  CliUsageError,
3
+ RuntimeApiError,
3
4
  printData,
4
5
  rejectArgs,
5
6
  runCliAction,
@@ -8,10 +9,16 @@ import {
8
9
  takeFlag,
9
10
  takeIntegerOption,
10
11
  takeOption,
12
+ takeOptionWithSyntax,
11
13
  type RuntimeApiDeps,
12
14
  } from "./runtime-api";
15
+ import { followLogs } from "./log-follow";
16
+ import { handleLogFilterCommand } from "./log-view-filter";
17
+ import { followInjection } from "./injection-follow";
18
+ import type { ObserveStreamDeps } from "./observe-stream";
13
19
  import { formatUsageReport } from "./usage-report";
14
- import { USAGE_RANGES, USAGE_SURFACES, type UsageSummary } from "../usage/summary";
20
+ import { selectUsageModelView, takeUsageSearchOption } from "./usage-model-search";
21
+ import { USAGE_RANGES, USAGE_SURFACES, type UsageSummary, type UsageFilterEcho } from "../usage/summary";
15
22
  import { parseUsageTimeWindow, type UsageTimeWindow } from "../usage/time-range";
16
23
  import { redactSecretString } from "../lib/redact";
17
24
  import { readClientConnectionState, sameClientConnectionOwner } from "../client/state";
@@ -21,18 +28,19 @@ import type { HubUsageReport } from "../remote/hub-usage";
21
28
 
22
29
  const USAGE = `Usage:
23
30
  ocx observe logs [--provider <name>] [--model <id>] [--status <code>]
24
- [--conversation <id>] [--account <label>] [--limit <n>] [--follow] [--json|--jsonl]
31
+ [--conversation <id>] [--account <label>] [--limit <n>] [--follow] [--json|--jsonl] [--events]
25
32
  ocx logs explain <request-id> [--json]
33
+ ocx logs filter [selectors] [--scan-limit <1..2000>] [--limit <1..2000>] [--json|--jsonl]
26
34
  ocx logs rebuild-index
27
35
  ocx logs index-status
28
36
  ocx observe usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>]
29
37
  [--since <epoch-ms|ISO-datetime>] [--until <epoch-ms|ISO-datetime>]
30
- [--provider <name>] [--model <id>] [--json]
38
+ [--provider <name>] [--model <id>] [--api-key-id <id>] [--search <text>] [--json]
31
39
  ocx observe storage [codex-logs [status|protect|unprotect|repair|compact] [--mode <compat|quiet>]] [--json]
32
40
  ocx observe memory [--json]
33
41
  ocx observe debug [--json]
34
42
  ocx observe claude-inbound [--limit <n>] [--json]
35
- ocx observe injection [--limit <n>] [--json]`;
43
+ ocx observe injection [--limit <n>] [--follow [--jsonl]] [--json]`;
36
44
 
37
45
  type LogEntry = Record<string, unknown> & { id?: string | number; timestamp?: string; provider?: string; model?: string; status?: number };
38
46
 
@@ -77,10 +85,11 @@ function formatLog(row: LogEntry): string {
77
85
  }
78
86
 
79
87
  /** Read or follow request logs while preserving raw JSON and JSONL output. */
80
- async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
88
+ async function logs(argv: string[], deps: ObserveStreamDeps): Promise<number> {
81
89
  const args = [...argv];
82
90
  const wantsJson = takeFlag(args, "--json");
83
- const wantsJsonl = takeFlag(args, "--jsonl");
91
+ const wantsEvents = takeFlag(args, "--events");
92
+ const wantsJsonl = takeFlag(args, "--jsonl") || wantsEvents;
84
93
  const follow = takeFlag(args, "--follow") || takeFlag(args, "-f");
85
94
  const provider = takeOption(args, "--provider");
86
95
  const model = takeOption(args, "--model");
@@ -97,24 +106,19 @@ async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
97
106
  if (follow && wantsJson) {
98
107
  throw new CliUsageError("--follow cannot be combined with --json; use --jsonl for streaming JSONL", USAGE);
99
108
  }
100
- let seen = new Set<string>();
101
- do {
102
- const data = await runtimeRequest(`/api/logs${query({ provider, model, status, conversationId, account, limit })}`, {}, deps);
103
- const rows = logRows(data);
104
- if (!follow && wantsJson) printData(data, true);
105
- else {
106
- for (const row of rows) {
107
- const key = String(row.id ?? `${row.timestamp}:${row.provider}:${row.model}:${row.status}`);
108
- if (follow && seen.has(key)) continue;
109
- if (wantsJsonl) console.log(JSON.stringify(row));
110
- else console.log(formatLog(row));
111
- seen.add(key);
112
- }
113
- }
114
- if (!follow) return;
115
- if (seen.size > 5_000) seen = new Set([...seen].slice(-2_500));
116
- await Bun.sleep(1_000);
117
- } while (true);
109
+ if (wantsEvents && !follow) throw new CliUsageError("--events requires --follow", USAGE);
110
+ const params = { provider, model, status, conversationId, account, limit };
111
+ if (follow) {
112
+ if (limit > 2000) throw new CliUsageError("--limit must be between 1 and 2000 for follow", USAGE);
113
+ return followLogs({ query: new URLSearchParams(query(params)), limit, jsonl: wantsJsonl, events: wantsEvents, formatRow: formatLog }, deps);
114
+ }
115
+ const data = await runtimeRequest(`/api/logs${query(params)}`, {}, deps);
116
+ if (wantsJson) printData(data, true);
117
+ else for (const row of logRows(data)) {
118
+ if (wantsJsonl) console.log(JSON.stringify(row));
119
+ else console.log(formatLog(row));
120
+ }
121
+ return 0;
118
122
  }
119
123
 
120
124
  async function explain(argv: string[], deps: RuntimeApiDeps): Promise<void> {
@@ -163,6 +167,7 @@ async function indexStatus(argv: string[], deps: RuntimeApiDeps): Promise<void>
163
167
 
164
168
  async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
165
169
  const args = [...argv];
170
+ const search = takeUsageSearchOption(args);
166
171
  const wantsJson = takeFlag(args, "--json");
167
172
  const range = takeOption(args, "--range") ?? "30d";
168
173
  const surface = takeOption(args, "--surface") ?? "all";
@@ -170,6 +175,9 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
170
175
  const model = takeOption(args, "--model");
171
176
  const since = takeOption(args, "--since");
172
177
  const until = takeOption(args, "--until");
178
+ const rawKeyId = takeOptionWithSyntax(args, "--api-key-id")?.value;
179
+ const apiKeyId = rawKeyId?.trim();
180
+ if (apiKeyId !== undefined && !apiKeyId) throw new CliUsageError("--api-key-id must not be blank", USAGE);
173
181
  let window: UsageTimeWindow | undefined;
174
182
  try {
175
183
  window = parseUsageTimeWindow(since, until);
@@ -184,13 +192,14 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
184
192
  throw new CliUsageError(`--surface must be one of ${USAGE_SURFACES.join(", ")}`, USAGE);
185
193
  }
186
194
  rejectArgs(args.map(redactSecretString), USAGE);
187
- const suffix = query({ range, surface, provider, model, since: window?.since, until: window?.until });
195
+ const suffix = query({ range, surface, provider, model, apiKeyId, since: window?.since, until: window?.until });
188
196
  const connection = readClientConnectionState();
189
- let result: UsageSummary | HubUsageReport;
197
+ let result: (UsageSummary & { filter?: UsageFilterEcho }) | HubUsageReport;
190
198
  if (connection.kind === "invalid" || connection.kind === "mismatched") {
191
199
  throw new Error(`Client usage unavailable: ${connection.reason}`);
192
200
  }
193
201
  if (connection.kind === "connected") {
202
+ if (apiKeyId !== undefined) throw new CliUsageError("--api-key-id is unavailable on connected clients; run on the hub for a selected key, or omit it for this client's usage", USAGE);
194
203
  const token = readServiceApiTokenState();
195
204
  if (token.kind !== "present" || token.fingerprint !== connection.value.tokenFingerprint) {
196
205
  throw new Error("Client usage unavailable: the enrolled data key is missing or changed; repair the client connection");
@@ -206,7 +215,18 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
206
215
  throw new Error("Client connection changed while reading usage; retry for the current connection");
207
216
  }
208
217
  } else {
209
- result = await runtimeRequest<UsageSummary>(`/api/usage${suffix}`, {}, deps);
218
+ if (apiKeyId === undefined) result = await runtimeRequest<UsageSummary>(`/api/usage${suffix}`, {}, deps);
219
+ else {
220
+ try {
221
+ result = await runtimeRequest<UsageSummary & { filter?: UsageFilterEcho }>(`/api/usage${suffix}`, { redirect: "error", credentials: "omit" }, deps);
222
+ } catch (error) {
223
+ if (error instanceof RuntimeApiError && error.code === "proxy_not_running") throw error;
224
+ throw new Error("Key-scoped usage could not be read. Check runtime access and retry.");
225
+ }
226
+ if (result?.filter?.apiKeyId !== apiKeyId) {
227
+ throw new Error("The server did not confirm the requested API key scope. Upgrade and restart the proxy, then retry.");
228
+ }
229
+ }
210
230
  }
211
231
  // Older daemons ignore custom bounds and return successful preset reports.
212
232
  if (window && (result?.customWindow !== true || result.since !== window.since || result.until !== window.until)) {
@@ -216,8 +236,10 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
216
236
  // the call, so passing formatUsageReport(...) inline would run the human
217
237
  // renderer during --json and let its assumptions affect a path that is meant
218
238
  // to bypass it entirely.
219
- if (wantsJson) printData(result, true);
220
- else printData(result, false, formatUsageReport(result as Parameters<typeof formatUsageReport>[0]));
239
+ const view = search === undefined ? undefined : selectUsageModelView(result, search);
240
+ const displayed = view ?? result;
241
+ if (wantsJson) printData(displayed, true);
242
+ else printData(displayed, false, formatUsageReport(displayed as Parameters<typeof formatUsageReport>[0], view ? { modelView: view.modelView } : undefined));
221
243
  }
222
244
 
223
245
  async function simple(path: string, argv: string[], deps: RuntimeApiDeps): Promise<void> {
@@ -265,24 +287,43 @@ async function storage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
265
287
  printData(result, wantsJson, summaryLines(result));
266
288
  }
267
289
 
268
- export async function handleObserveCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
269
- return runCliAction(async () => {
290
+ async function injection(argv: string[], deps: ObserveStreamDeps): Promise<number> {
291
+ const args = [...argv];
292
+ const follow = takeFlag(args, "--follow");
293
+ const jsonl = takeFlag(args, "--jsonl");
294
+ if (!follow) {
295
+ if (jsonl) throw new CliUsageError("--jsonl requires --follow for injection logs", USAGE);
296
+ await simple("/api/debug/injection-logs", args, deps);
297
+ return 0;
298
+ }
299
+ if (takeFlag(args, "--json")) throw new CliUsageError("--follow cannot be combined with --json; use --jsonl", USAGE);
300
+ const limit = takeIntegerOption(args, "--limit", { min: 1 }) ?? 500;
301
+ if (limit > 2000) throw new CliUsageError("--limit must be between 1 and 2000 for follow", USAGE);
302
+ rejectArgs(args, USAGE);
303
+ return followInjection(limit, jsonl, deps);
304
+ }
305
+
306
+ export async function handleObserveCommand(argv: string[], deps: ObserveStreamDeps = {}): Promise<number> {
307
+ let streamExit = 0;
308
+ const exit = await runCliAction(async () => {
270
309
  const [sub = "logs", ...rest] = argv;
271
310
  if (sub === "logs") {
272
311
  const action = rest[0];
273
- if (action === "explain") await explain(rest.slice(1), deps);
312
+ if (action === "filter") streamExit = await handleLogFilterCommand(rest.slice(1), deps, formatLog);
313
+ else if (action === "explain") await explain(rest.slice(1), deps);
274
314
  else if (action === "rebuild-index") await rebuildIndex(rest.slice(1), deps);
275
315
  else if (action === "index-status") await indexStatus(rest.slice(1), deps);
276
- else await logs(rest, deps);
316
+ else streamExit = await logs(rest, deps);
277
317
  }
278
318
  else if (sub === "usage") await usage(rest, deps);
279
319
  else if (sub === "storage") await storage(rest, deps);
280
320
  else if (sub === "memory") await simple("/api/system/memory", rest, deps);
281
321
  else if (sub === "debug") await simple("/api/debug", rest, deps);
282
322
  else if (sub === "claude-inbound") await simple("/api/claude/inbound-debug", rest, deps);
283
- else if (sub === "injection") await simple("/api/debug/injection-logs", rest, deps);
323
+ else if (sub === "injection") streamExit = await injection(rest, deps);
284
324
  else throw new CliUsageError(`unknown observe command ${sub}`, USAGE);
285
325
  });
326
+ return exit || streamExit;
286
327
  }
287
328
 
288
329
  export const OBSERVE_USAGE = USAGE;
@@ -39,6 +39,7 @@ import type {
39
39
  OpencodeProviderBlocks,
40
40
  OpencodeV2ProviderBlock,
41
41
  } from "../clients/config-export";
42
+ import type { EffectiveModelExportMetadata } from "../clients/config-export/contracts";
42
43
  import { filterCatalogVisibleModels, visibleNativeSlugs } from "../codex/catalog";
43
44
  import { commandInvocation } from "../lib/win-exec";
44
45
  import { configuredAdminToken, opencodeCatalogToken } from "../lib/admin-secrets";
@@ -106,12 +107,30 @@ export interface OpencodeProxyModelRow {
106
107
  contextWindow?: number;
107
108
  /** Authoritative output limit (CatalogModel.maxOutputTokens); optional. */
108
109
  maxOutputTokens?: number;
110
+ /** Authoritative input ceiling (CatalogModel.maxInputTokens); optional. */
111
+ maxInputTokens?: number;
112
+ /**
113
+ * Derived on the hub from known evidence; absent means unknown, never a negative. An
114
+ * explicit `false` from the hub is evidence too and is carried through unchanged.
115
+ */
116
+ supportsTools?: boolean;
117
+ supportsReasoning?: boolean;
118
+ supportsReasoningSummaries?: boolean;
109
119
  /** Declared input modalities from `/api/models`; carried into opencode model capabilities. */
110
120
  inputModalities?: string[];
111
121
  /** Declared effort ladder from `/api/models`; carried into opencode model variants. */
112
122
  reasoningEfforts?: string[];
113
123
  /** Declared default effort from `/api/models`. */
114
124
  defaultReasoningEffort?: string;
125
+ /**
126
+ * Effective export metadata `/api/models` attaches to every row it produces. Custom row
127
+ * fields are the operator's stored overrides, so this object is the authoritative
128
+ * projection a client consumes; routed and native rows carry their declared default in it
129
+ * because the row-level default is the picker's preference order. When present, an absent
130
+ * field inside means "no declared value" — never "fall back to the row" — and every field
131
+ * below still works for rows without it (older hubs, hand-built rows).
132
+ */
133
+ exportMetadata?: EffectiveModelExportMetadata;
115
134
  }
116
135
 
117
136
  const PROJECT_CONFIG_FILENAMES = ["opencode.json", "opencode.jsonc"] as const;
@@ -324,6 +343,12 @@ export async function fetchOpencodeProxyModels(
324
343
  /**
325
344
  * Visible OpenCode catalog entries from proxy /api/models rows. Disabled rows are omitted;
326
345
  * native rows are omitted in Codex Direct mode.
346
+ *
347
+ * Custom rows carry their effective projection in `exportMetadata`; when it is present it
348
+ * wins over the row's own fields, which for a custom row are the operator's stored overrides
349
+ * rather than the resolved capabilities. Routed/native projections also distinguish declared
350
+ * defaults from picker preferences; older hubs without the field retain their row values. The empty-vs-undefined ladder distinction
351
+ * survives: a row declaring `reasoningEfforts: []` exports `[]`, not an absent key.
327
352
  */
328
353
  export function opencodeCatalogFromProxyRows(
329
354
  rows: readonly OpencodeProxyModelRow[],
@@ -343,24 +368,43 @@ export function opencodeCatalogFromProxyRows(
343
368
  && !visibleRouted.has(row)) continue;
344
369
  if (seen.has(namespaced)) continue;
345
370
  seen.add(namespaced);
371
+ const effective = row.exportMetadata;
372
+ const maxTokens = effective?.maxTokens ?? row.maxOutputTokens;
373
+ const maxInputTokens = effective?.maxInputTokens ?? row.maxInputTokens;
374
+ const inputModalities = effective?.inputModalities ?? row.inputModalities;
375
+ const reasoningEfforts = effective?.reasoningEfforts ?? row.reasoningEfforts;
376
+ // The projection is the authority on the default: when it exists, an absent default means
377
+ // NO declared default, and falling back to the row would reintroduce the picker's
378
+ // synthetic medium/low value. Rows without one (older hubs, hand-built rows) keep the
379
+ // row's own value.
380
+ const defaultReasoningEffort = effective !== undefined
381
+ ? effective.defaultReasoningEffort
382
+ : row.defaultReasoningEffort;
383
+ const supportsTools = effective?.supportsTools ?? row.supportsTools;
384
+ const supportsReasoning = effective?.supportsReasoning ?? row.supportsReasoning;
385
+ const supportsReasoningSummaries = effective?.supportsReasoningSummaries ?? row.supportsReasoningSummaries;
346
386
  catalog.push({
347
387
  namespaced,
348
388
  native: row.native === true,
349
389
  provider: row.provider,
350
390
  id: row.id,
351
- contextWindow: row.contextWindow,
352
- ...(typeof row.maxOutputTokens === "number" ? { maxTokens: row.maxOutputTokens } : {}),
391
+ contextWindow: effective?.contextWindow ?? row.contextWindow,
392
+ ...(typeof maxTokens === "number" ? { maxTokens } : {}),
393
+ ...(typeof maxInputTokens === "number" ? { maxInputTokens } : {}),
353
394
  displayName: row.displayNameSource === "fallback" ? undefined : row.displayName,
354
- ...(Array.isArray(row.inputModalities) && row.inputModalities.length > 0
355
- ? { inputModalities: [...row.inputModalities] }
395
+ ...(Array.isArray(inputModalities) && inputModalities.length > 0
396
+ ? { inputModalities: [...inputModalities] }
356
397
  : {}),
357
398
  ...(typeof row.fastRowAvailable === "boolean" ? { fastRowAvailable: row.fastRowAvailable } : {}),
358
- ...(Array.isArray(row.reasoningEfforts) && row.reasoningEfforts.length > 0
359
- ? { reasoningEfforts: [...row.reasoningEfforts] }
360
- : {}),
361
- ...(typeof row.defaultReasoningEffort === "string" && row.defaultReasoningEffort.length > 0
362
- ? { defaultReasoningEffort: row.defaultReasoningEffort }
399
+ // `[]` is a declaration, not an absence: an explicit no-rungs ladder stays empty here.
400
+ ...(Array.isArray(reasoningEfforts) ? { reasoningEfforts: [...reasoningEfforts] } : {}),
401
+ ...(typeof defaultReasoningEffort === "string" && defaultReasoningEffort.length > 0
402
+ ? { defaultReasoningEffort }
363
403
  : {}),
404
+ // An explicit false survives the hop; only derivation is true-or-nothing.
405
+ ...(typeof supportsTools === "boolean" ? { supportsTools } : {}),
406
+ ...(typeof supportsReasoning === "boolean" ? { supportsReasoning } : {}),
407
+ ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
364
408
  });
365
409
  }
366
410
  return catalog;
@@ -377,7 +421,7 @@ export function isOpencodeRuntimeConfigError(
377
421
 
378
422
  /**
379
423
  * Merge inherited `OPENCODE_CONFIG_CONTENT` and override only our own blocks:
380
- * `provider.opencodex` (V1) and `providers.opencodex` (V2, the one carrying variants).
424
+ * `provider.opencodex` (V1 maps) and `providers.opencodex` (native V2 arrays).
381
425
  * When no inline layer is present, emit the minimal runtime object for this launcher.
382
426
  */
383
427
  export function mergeOpencodeRuntimeConfig(
@@ -0,0 +1,77 @@
1
+ import { parseProviderEditorConfigDTO } from "../server/auth-cors";
2
+ import { readJsonInput, serializeManagementJson } from "./json-input";
3
+ import { printProviderReceipt, runProviderAction } from "./provider-result";
4
+ import {
5
+ CliUsageError, printData, runtimeBaseUrl, runtimeRequest, takeFlag,
6
+ takeOptionWithSyntax, type RuntimeApiDeps,
7
+ } from "./runtime-api";
8
+
9
+ function record(value: unknown): value is Record<string, unknown> {
10
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
11
+ const prototype = Object.getPrototypeOf(value);
12
+ return prototype === Object.prototype || prototype === null;
13
+ }
14
+
15
+ function editorInput(value: unknown) {
16
+ const parsed = parseProviderEditorConfigDTO(value);
17
+ if (!parsed.ok) throw new CliUsageError("Provider batch input must be an editable provider snapshot without secret, derived or unknown fields.");
18
+ return parsed.value;
19
+ }
20
+
21
+ async function snapshot(wantsJson: boolean, deps: RuntimeApiDeps): Promise<number> {
22
+ const baseUrl = await runtimeBaseUrl(deps);
23
+ const fetched = await runtimeRequest("/api/config", { method: "GET", redirect: "error" }, { ...deps, baseUrl });
24
+ if (!record(fetched) || !record(fetched.providers)) throw new Error("Invalid provider snapshot response.");
25
+ const entries = Object.entries(fetched.providers).map(([name, provider]) => {
26
+ if (!record(provider)) throw new Error("Invalid provider snapshot response.");
27
+ // These are the four dashboard decorations, not editor fields. Any other
28
+ // unexpected field must reach the canonical parser and fail closed.
29
+ const { hasApiKey, hasHeaders, xaiResponsesOptInState, initialModelSelection, ...editable } = provider;
30
+ return [name, editable] as const;
31
+ });
32
+ const parsed = parseProviderEditorConfigDTO({
33
+ defaultProvider: fetched.defaultProvider,
34
+ providers: Object.fromEntries(entries),
35
+ });
36
+ if (!parsed.ok) throw new Error("Invalid provider snapshot response.");
37
+ printData(parsed.value, wantsJson);
38
+ return 0;
39
+ }
40
+
41
+ async function apply(args: string[], wantsJson: boolean, deps: RuntimeApiDeps): Promise<number> {
42
+ const baselinePath = takeOptionWithSyntax(args, "--baseline")?.value;
43
+ const nextPath = takeOptionWithSyntax(args, "--file")?.value;
44
+ const confirmed = takeFlag(args, "--yes");
45
+ if (args.length || !baselinePath?.trim() || !nextPath?.trim()) {
46
+ throw new CliUsageError("Provider apply requires --baseline FILE and --file FILE, with optional --yes and --json.");
47
+ }
48
+ if (baselinePath === "-" && nextPath === "-") {
49
+ throw new CliUsageError("Provider apply accepts at most one stdin source.");
50
+ }
51
+ // Parse both documents as supplied: normalizing a public baseline would
52
+ // change the server's CAS comparison and can discard unseen private data.
53
+ const baseline = editorInput(await readJsonInput(baselinePath, deps, "Provider baseline"));
54
+ const next = editorInput(await readJsonInput(nextPath, deps, "Provider next snapshot"));
55
+ if (!confirmed && Object.keys(baseline.providers).some(name => !Object.hasOwn(next.providers, name))) {
56
+ throw new CliUsageError("Provider removals or renames require --yes. Batch apply does not perform single-provider DELETE OAuth account cleanup.");
57
+ }
58
+ const body = serializeManagementJson({ baseline, next });
59
+ const baseUrl = await runtimeBaseUrl(deps);
60
+ const result = await runtimeRequest("/api/providers", {
61
+ method: "PUT", redirect: "error", headers: { "content-type": "application/json" }, body,
62
+ }, { ...deps, baseUrl });
63
+ return printProviderReceipt(result, wantsJson, "Provider batch");
64
+ }
65
+
66
+ /** Public editor snapshots and one server-authoritative compare-and-swap. */
67
+ export async function handleProviderBatchCommand(
68
+ sub: "snapshot" | "apply", args: string[], deps: RuntimeApiDeps = {},
69
+ ): Promise<number> {
70
+ return runProviderAction(async () => {
71
+ const argv = [...args];
72
+ const wantsJson = takeFlag(argv, "--json");
73
+ if (sub === "apply") return apply(argv, wantsJson, deps);
74
+ if (argv.length) throw new CliUsageError("Provider snapshot accepts only --json.");
75
+ return snapshot(wantsJson, deps);
76
+ });
77
+ }