@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
@@ -1,7 +1,10 @@
1
1
  // Shared client export contracts.
2
+ import { canonicalizeReasoningEfforts } from "../../reasoning-effort";
2
3
  import type { OcxConfig } from "../../types";
3
4
  import type { ConfigFormat } from "../../integrations/serialize";
4
5
 
6
+ export const DROID_DEFAULT_EFFORT_HEADER = "x-opencodex-droid-default-effort";
7
+
5
8
  /**
6
9
  * One entry opencodex owns inside a client's config: the JSON path to it and
7
10
  * the value we put there.
@@ -38,6 +41,12 @@ export interface OpencodeCatalogModel {
38
41
  id?: string;
39
42
  contextWindow?: number;
40
43
  maxTokens?: number;
44
+ /**
45
+ * Authoritative input ceiling, distinct from `contextWindow` (GPT-5.6 advertises 922k
46
+ * usable input under a 1.05M window). Carried from the catalog row so a client that has a
47
+ * separate input limit never treats the whole window as prompt budget.
48
+ */
49
+ maxInputTokens?: number;
41
50
  displayName?: string;
42
51
  /**
43
52
  * Declared input modalities, carried verbatim from `/api/models`. Serialized as opencode's
@@ -54,6 +63,9 @@ export interface OpencodeCatalogModel {
54
63
  * model-level setting — see {@link opencodeEffortVariants} for why.
55
64
  */
56
65
  defaultReasoningEffort?: string;
66
+ supportsTools?: boolean;
67
+ supportsReasoning?: boolean;
68
+ supportsReasoningSummaries?: boolean;
57
69
  }
58
70
 
59
71
  /**
@@ -73,10 +85,94 @@ export interface ExportModel {
73
85
  displayName?: string;
74
86
  contextWindow?: number;
75
87
  maxTokens?: number;
88
+ /** Authoritative input ceiling; optional and never guessed from the context window. */
89
+ maxInputTokens?: number;
76
90
  inputModalities?: string[];
77
91
  /** Optional effort ladder exported only to clients that support it. */
78
92
  reasoningEfforts?: string[];
79
93
  defaultReasoningEffort?: string;
94
+ supportsTools?: boolean;
95
+ supportsReasoning?: boolean;
96
+ supportsReasoningSummaries?: boolean;
97
+ }
98
+
99
+ /**
100
+ * Effective, override-applied export metadata for one model row: what a client export should
101
+ * serialize, as distinct from the raw editor representation `/api/models` also carries.
102
+ *
103
+ * Custom rows are the reason this exists. Their row fields are the operator's stored
104
+ * OVERRIDES (an absent field means "not overridden", an empty ladder means "no rungs"), so a
105
+ * serializer reading only them drops every capability the model inherits from the provider,
106
+ * the registry, or the gathered catalog. The management row list resolves that projection
107
+ * from the same canonical sources routed rows use and attaches it additively — the raw
108
+ * override fields stay exactly what the editor wrote.
109
+ */
110
+ export interface EffectiveModelExportMetadata {
111
+ contextWindow?: number;
112
+ /** Input ceiling; distinct from `contextWindow`, carried only when a source asserts one. */
113
+ maxInputTokens?: number;
114
+ /** Output ceiling (a catalog row's `maxOutputTokens`). */
115
+ maxTokens?: number;
116
+ inputModalities?: string[];
117
+ /** Effective ladder; `[]` is an explicit "no rungs" declaration and survives as `[]`. */
118
+ reasoningEfforts?: string[];
119
+ /**
120
+ * A default some canonical source declared — never one synthesized from the ladder's
121
+ * preference order, and only when it is a member of the effective ladder. An empty
122
+ * ladder suppresses it entirely: there is no rung to default to.
123
+ */
124
+ defaultReasoningEffort?: string;
125
+ supportsTools?: boolean;
126
+ supportsReasoning?: boolean;
127
+ supportsReasoningSummaries?: boolean;
128
+ }
129
+
130
+ /**
131
+ * The per-row evidence the capability booleans are derived from. Only positive, canonical
132
+ * signals — no independent provider fact tables, no name-based inference.
133
+ */
134
+ export interface ExportCapabilityEvidence {
135
+ native?: boolean;
136
+ reasoningEfforts?: readonly string[];
137
+ supportsReasoningSummaries?: boolean;
138
+ /** Normalized upstream capability names a catalog row carried (e.g. `"tools"`). */
139
+ capabilities?: readonly string[];
140
+ /** Provider-level parallel tool call opt-in for this model. */
141
+ parallelToolCalls?: boolean;
142
+ }
143
+
144
+ /**
145
+ * Tool support a canonical source positively asserts, or undefined when nothing is known.
146
+ *
147
+ * Mirrors the routing reader (`src/routing/capability.ts`): a catalog row without `"tools"`
148
+ * is UNKNOWN, never a negative — the row may simply not enumerate capabilities. `native`
149
+ * rows and the parallel-call opt-in are positive evidence on their own.
150
+ */
151
+ export function knownToolsSupport(evidence: ExportCapabilityEvidence): boolean | undefined {
152
+ if (evidence.native === true) return true;
153
+ if (evidence.capabilities?.includes("tools") === true) return true;
154
+ if (evidence.parallelToolCalls === true) return true;
155
+ return undefined;
156
+ }
157
+
158
+ /**
159
+ * Reasoning support a canonical source positively asserts, or undefined when nothing is
160
+ * known.
161
+ *
162
+ * Never `false` from a missing or empty effort ladder: an empty ladder says "no ADJUSTABLE
163
+ * effort", which is not the claim "cannot reason" — a model can reason at a fixed depth with
164
+ * no rung to select. Known evidence is a positive reasoning rung (the catalog's own statement
165
+ * that the model accepts reasoning parameters) or delivered reasoning summaries. `none` is
166
+ * the off sentinel, not a rung: a ladder of only `none` offers an off variant and asserts
167
+ * nothing about whether the model reasons, so it is not positive evidence on its own.
168
+ */
169
+ export function knownReasoningSupport(evidence: ExportCapabilityEvidence): boolean | undefined {
170
+ if (evidence.supportsReasoningSummaries === true) return true;
171
+ if (evidence.reasoningEfforts !== undefined
172
+ && canonicalizeReasoningEfforts(evidence.reasoningEfforts).some(effort => effort !== "none")) {
173
+ return true;
174
+ }
175
+ return undefined;
80
176
  }
81
177
 
82
178
  export interface ExportContext {
@@ -88,8 +184,12 @@ export interface ExportContext {
88
184
  * admission from `apiKey` to the `x-opencodex-api-key` header.
89
185
  */
90
186
  config?: OcxConfig;
187
+ droidReasoningDefaults?: DroidReasoningDefaults;
91
188
  }
92
189
 
190
+ /** Namespaced model selector to one of that model's declared reasoning efforts. */
191
+ export type DroidReasoningDefaults = Record<string, string>;
192
+
93
193
  export type ExportClientId =
94
194
  | "opencode"
95
195
  | "pi"
@@ -1,9 +1,11 @@
1
1
  import { win32, join } from "node:path";
2
2
  import { homedir } from "node:os";
3
3
  import { exportPresentationLabel } from "../model-presentation";
4
- import type { ExportContext, ManagedContribution, OpencodeLaunchEnv } from "./contracts";
4
+ import type { DroidReasoningDefaults, ExportContext, ExportModel, ManagedContribution, OpencodeLaunchEnv } from "./contracts";
5
5
  import { normalizeExportModels } from "./model-metadata";
6
- import { formatSelectorConjunction } from "../../integrations/merge";
6
+ import { formatSelectorConjunction, readPath } from "../../integrations/merge";
7
+
8
+ import { DROID_DEFAULT_EFFORT_HEADER } from "./contracts";
7
9
 
8
10
  /** Factory personal settings: https://docs.factory.ai/model-independence/byok */
9
11
  export interface DroidModelEntry {
@@ -12,6 +14,7 @@ export interface DroidModelEntry {
12
14
  baseUrl: string;
13
15
  provider: "generic-chat-completion-api";
14
16
  noImageSupport: boolean;
17
+ extraHeaders?: Record<string, string>;
15
18
  }
16
19
 
17
20
  export interface DroidGeneratedConfig { customModels: DroidModelEntry[] }
@@ -43,11 +46,60 @@ function buildDroidRows(ctx: ExportContext): Array<{ row: DroidModelEntry; selec
43
46
  baseUrl: ctx.baseUrl,
44
47
  provider: "generic-chat-completion-api",
45
48
  noImageSupport: !model.inputModalities?.includes("image"),
49
+ ...(ctx.droidReasoningDefaults && Object.hasOwn(ctx.droidReasoningDefaults, model.namespaced)
50
+ ? { extraHeaders: { [DROID_DEFAULT_EFFORT_HEADER]: ctx.droidReasoningDefaults[model.namespaced]! } }
51
+ : {}),
46
52
  } });
47
53
  }
48
54
  return rows;
49
55
  }
50
56
 
57
+ export function droidReasoningModels(models: readonly ExportModel[]): Array<{ model: string; label: string; efforts: string[] }> {
58
+ const effortsByModel = new Map(normalizeExportModels(models).map(model => [model.namespaced, model.reasoningEfforts ?? []]));
59
+ return buildDroidRows({ baseUrl: "", models }).map(({ row }) => ({
60
+ model: row.model,
61
+ label: row.displayName.replace(/^OpenCodex: /, ""),
62
+ efforts: effortsByModel.get(row.model) ?? [],
63
+ }));
64
+ }
65
+
66
+ export function droidDefaultsFromOwnedRows(
67
+ ctx: ExportContext,
68
+ document: unknown,
69
+ fragmentPaths: readonly (readonly string[])[],
70
+ ): DroidReasoningDefaults {
71
+ const exportableModels = new Map(droidReasoningModels(ctx.models).map(model => [model.model, model.efforts]));
72
+ const defaults: DroidReasoningDefaults = {};
73
+ for (const path of fragmentPaths) {
74
+ if (path.length !== 2 || path[0] !== "customModels") continue;
75
+ const value = readPath(document, path);
76
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
77
+ const current = value as Record<string, unknown>;
78
+ if (typeof current.model !== "string" || !exportableModels.has(current.model)) continue;
79
+ const headers = current.extraHeaders;
80
+ if (!headers || typeof headers !== "object" || Array.isArray(headers)) continue;
81
+ const header = headers as Record<string, unknown>;
82
+ const effort = header[DROID_DEFAULT_EFFORT_HEADER];
83
+ if (typeof effort !== "string" || !exportableModels.get(current.model)?.includes(effort)) continue;
84
+ defaults[current.model] = effort;
85
+ }
86
+ return defaults;
87
+ }
88
+
89
+ export function validateDroidReasoningDefaults(
90
+ models: readonly ExportModel[],
91
+ defaults: unknown,
92
+ ): string | null {
93
+ if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) return "droidReasoningDefaults must be an object";
94
+ const addressable = new Map(droidReasoningModels(models).map(model => [model.model, model.efforts]));
95
+ for (const [model, effort] of Object.entries(defaults)) {
96
+ const efforts = addressable.get(model);
97
+ if (!efforts) return `model ${model} is not currently exportable to Droid`;
98
+ if (typeof effort !== "string" || !efforts.includes(effort)) return `effort for ${model} is not declared by that model`;
99
+ }
100
+ return null;
101
+ }
102
+
51
103
  export function buildDroidClientConfig(ctx: ExportContext): DroidGeneratedConfig {
52
104
  const rows = buildDroidRows(ctx);
53
105
  if (ctx.models.length > 0 && rows.length === 0) {
@@ -130,3 +130,24 @@ export function buildDshContribution(ctx: ExportContext): ManagedContribution {
130
130
  const doc = buildDshClientConfig(ctx);
131
131
  return singleFragment("dsh", ["llm-pi-ai", "providers", OPENCODE_PROVIDER_ID], doc["llm-pi-ai"].providers[OPENCODE_PROVIDER_ID]);
132
132
  }
133
+
134
+ /**
135
+ * Where DSH 0.1.7+ reads provider routes: the `llm-pi-ai` row of a profile
136
+ * patch, a top-level YAML list of loader rows. `$DSH_HOME/settings.yaml` is
137
+ * imported into the first profile that boots and renamed, so a write there
138
+ * reaches DSH once at most. The Desktop app runs the `desktop` profile and
139
+ * owns it alone, and DSH hot reloads that file when it changes.
140
+ */
141
+ export const DSH_DESKTOP_PROFILE = "desktop";
142
+ export const DSH_PROFILE_PROVIDER_PATH = ["[id=llm-pi-ai]", "config", "providers", OPENCODE_PROVIDER_ID] as const;
143
+
144
+ /** A profile patch DSH itself would load: a list whose every row is a mapping. */
145
+ export function dshProfilePatchEstablished(parsed: unknown): boolean {
146
+ return Array.isArray(parsed)
147
+ && parsed.every(row => typeof row === "object" && row !== null && !Array.isArray(row));
148
+ }
149
+
150
+ export function buildDshProfilePatchContribution(ctx: ExportContext): ManagedContribution {
151
+ const doc = buildDshClientConfig(ctx);
152
+ return singleFragment("dsh", DSH_PROFILE_PROVIDER_PATH, doc["llm-pi-ai"].providers[OPENCODE_PROVIDER_ID]);
153
+ }
@@ -14,9 +14,11 @@ import {
14
14
  normalizeExportModels,
15
15
  opencodeModelCapabilities,
16
16
  outputBudgetFor,
17
+ inputBudgetFor,
17
18
  proxyAdmissionHeaders,
18
19
  singleFragment,
19
20
  } from "./model-metadata";
21
+ import { legacyReasoningMetadata, type LegacyEffortVariant } from "./reasoning-metadata";
20
22
 
21
23
  export const KILO_CONFIG_CANDIDATES = [
22
24
  "kilo.jsonc",
@@ -28,9 +30,14 @@ export const KILO_CONFIG_CANDIDATES = [
28
30
 
29
31
  export interface KiloModelEntry {
30
32
  name: string;
31
- limit?: { context: number; output: number };
33
+ limit?: { context: number; input?: number; output: number };
32
34
  attachment?: boolean;
33
35
  modalities?: { input: string[]; output: string[] };
36
+ tool_call?: boolean;
37
+ reasoning?: boolean;
38
+ interleaved?: { field: "reasoning_content" };
39
+ options?: { reasoningEffort: string };
40
+ variants?: Record<string, LegacyEffortVariant>;
34
41
  }
35
42
 
36
43
  export interface KiloProviderBlock {
@@ -77,11 +84,14 @@ function kiloProviderBlock(ctx: ExportContext): KiloProviderBlock {
77
84
  const config = ctx.config ?? OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG;
78
85
  const models: Record<string, KiloModelEntry> = {};
79
86
  for (const model of normalizeExportModels(ctx.models)) {
80
- const entry: KiloModelEntry = { name: exportModelLabel(model) };
87
+ const entry: KiloModelEntry = { name: exportModelLabel(model), ...legacyReasoningMetadata(model) };
81
88
  const context = authoritativeContextWindow(model.contextWindow);
82
89
  if (context !== undefined) {
83
90
  entry.limit = { context, output: outputBudgetFor(context, model) };
91
+ const input = inputBudgetFor(context, model);
92
+ if (input !== undefined) entry.limit.input = input;
84
93
  }
94
+ if (typeof model.supportsTools === "boolean") entry.tool_call = model.supportsTools;
85
95
  const capabilities = opencodeModelCapabilities(model.inputModalities);
86
96
  if (capabilities) {
87
97
  entry.attachment = capabilities.attachment;
@@ -29,6 +29,12 @@ export function outputBudgetFor(context: number, model: OpencodeCatalogModel): n
29
29
  return Math.min(limit, context);
30
30
  }
31
31
 
32
+ /** Input limits are optional: unlike output, a missing limit needs no schema fallback. */
33
+ export function inputBudgetFor(context: number, model: OpencodeCatalogModel): number | undefined {
34
+ const limit = authoritativeContextWindow(model.maxInputTokens);
35
+ return limit === undefined ? undefined : Math.min(limit, context);
36
+ }
37
+
32
38
  /**
33
39
  * Modalities a given client's schema will actually accept.
34
40
  *
@@ -89,12 +95,12 @@ export function inputModalitiesForClient(
89
95
  const OPENCODE_INPUT_MODALITIES: ReadonlySet<string> = new Set(["text", "audio", "image", "video", "pdf"]);
90
96
 
91
97
  /**
92
- * opencode's per-model capability fields for one catalog row, or `undefined` when the row
98
+ * Legacy per-model modality fields for one catalog row, or `undefined` when the row
93
99
  * declares nothing.
94
100
  *
95
- * `undefined` rather than `{ input: ["text"] }`: opencode already computes an entry without
96
- * capabilities as text-only, and leaving the keys out keeps every model that declares
97
- * nothing byte-identical to what shipped before. A declared list is carried across as-is, so
101
+ * `undefined` rather than guessing `{ input: ["text"] }`: unknown capabilities leave the
102
+ * client's fallback assumptions in place. V2 translates these lists into native capability
103
+ * fields; legacy clients also receive the attachment flag. A declared list is carried across as-is, so
98
104
  * an audio-only row keeps `attachment: true` instead of being rewritten to text it cannot
99
105
  * read — the same call Pi's exporter makes, in the opposite direction.
100
106
  */
@@ -0,0 +1,47 @@
1
+ import { canonicalizeReasoningEfforts, isDeclaredReasoningEffort } from "../../reasoning-effort";
2
+ import { knownReasoningSupport, type OpencodeCatalogModel } from "./contracts";
3
+
4
+ /** Unknown and explicitly empty ladders have different client-side meanings. */
5
+ export function exportReasoningEfforts(model: OpencodeCatalogModel): string[] | undefined {
6
+ return model.reasoningEfforts === undefined ? undefined : canonicalizeReasoningEfforts(model.reasoningEfforts);
7
+ }
8
+
9
+ /** Carry an authoritative default, never invent one or revive a cleared ladder. */
10
+ export function exportDefaultReasoningEffort(model: OpencodeCatalogModel): string | undefined {
11
+ const effort = model.defaultReasoningEffort;
12
+ if (typeof effort !== "string" || !isDeclaredReasoningEffort(effort)) return undefined;
13
+ const efforts = exportReasoningEfforts(model);
14
+ return efforts === undefined || efforts.includes(effort) ? effort : undefined;
15
+ }
16
+
17
+ export type LegacyEffortVariant = { reasoningEffort: string } | { disabled: true };
18
+
19
+ /** Shared V1-family controls. A disabled-only map suppresses client-invented effort ladders. */
20
+ export function legacyReasoningMetadata(model: OpencodeCatalogModel): {
21
+ reasoning?: boolean;
22
+ options?: { reasoningEffort: string };
23
+ interleaved?: { field: "reasoning_content" };
24
+ variants?: Record<string, LegacyEffortVariant>;
25
+ } {
26
+ const efforts = exportReasoningEfforts(model);
27
+ const reasoning = model.supportsReasoning ?? knownReasoningSupport({
28
+ ...model, reasoningEfforts: efforts,
29
+ });
30
+ const defaultEffort = exportDefaultReasoningEffort(model);
31
+ // Both legacy runtimes merge generated variants with config, then filter disabled IDs.
32
+ // An unrelated sentinel leaves their generated choices alive. Disable the actual IDs
33
+ // instead (live-tested on Kilo 7.8.3, checked against stock OpenCode V1's merge).
34
+ const suppressed: Record<string, LegacyEffortVariant> = Object.fromEntries(
35
+ ["none", "minimal", "low", "medium", "high", "xhigh", "max"].map(id => [id, { disabled: true }]),
36
+ );
37
+ const variants = efforts?.length
38
+ ? Object.fromEntries(efforts.map(effort => [effort, { reasoningEffort: effort }]))
39
+ : reasoning === true ? suppressed : undefined;
40
+ return {
41
+ ...(typeof reasoning === "boolean" ? { reasoning } : {}),
42
+ ...(defaultEffort !== undefined ? { options: { reasoningEffort: defaultEffort } } : {}),
43
+ ...(reasoning === true || model.supportsReasoningSummaries === true
44
+ ? { interleaved: { field: "reasoning_content" } } : {}),
45
+ ...(variants !== undefined ? { variants } : {}),
46
+ };
47
+ }
@@ -30,7 +30,7 @@ import { probeHostname } from "../server/proxy-liveness";
30
30
  import type { OcxConfig } from "../types";
31
31
 
32
32
  export type { ConfigFormat } from "../integrations/serialize";
33
- export type { ManagedFragment, ManagedContribution, BuildContribution, OpencodeLaunchEnv, OpencodeCatalogModel, ExportModel, ExportContext, ExportClientId, ExportClientSpec, PiModelEntry } from "./config-export/contracts";
33
+ export type { ManagedFragment, ManagedContribution, BuildContribution, OpencodeLaunchEnv, OpencodeCatalogModel, ExportModel, ExportContext, ExportClientId, ExportClientSpec, PiModelEntry, DroidReasoningDefaults } from "./config-export/contracts";
34
34
  export { OPENCODE_PROVIDER_ID, OPENCODE_CONFIG_SCHEMA, OPENCODE_API_KEY_ENV, OPENCODE_API_KEY_ENV_REF, KILO_API_KEY_ENV, KILO_API_KEY_ENV_REF, KILO_CONFIG_SCHEMA, HERMES_API_KEY_ENV, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV, OPENCLAW_API_KEY_ENV_REF, LOOPBACK_API_KEY_PLACEHOLDER, GAJAE_API_KEY_ENV, SCHEMA_REQUIRED_OUTPUT_BUDGET, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG } from "./config-export/constants";
35
35
  export { normalizeExportModels } from "./config-export/model-metadata";
36
36
  export type { OmpModelEntry, OmpProviderBlock, OmpGeneratedConfig } from "./config-export/omp";
@@ -48,17 +48,19 @@ export {
48
48
  zcodeStoreSchemaEstablished,
49
49
  } from "./config-export/zcode-store";
50
50
  export type { DshReasoningEffort, DshWireReasoningEffort, DshModelEntry, DshProviderBlock, DshGeneratedConfig } from "./config-export/dsh";
51
+ export { DSH_PROFILE_PROVIDER_PATH, dshProfilePatchEstablished, buildDshProfilePatchContribution } from "./config-export/dsh";
51
52
  export type { McodeProviderBlock, McodeModelEntry, McodeGeneratedConfig } from "./config-export/mcode";
52
53
  export type { RaycastAbility, RaycastAbilityName, RaycastModelEntry, RaycastProviderEntry, RaycastGeneratedConfig } from "./config-export/raycast";
53
54
  export { buildRaycastClientConfig, summarizeRaycast, buildRaycastContribution } from "./config-export/raycast";
54
- export { droidHomeDir, droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidContribution } from "./config-export/droid";
55
+ export { droidHomeDir, droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidContribution, droidReasoningModels, droidDefaultsFromOwnedRows, validateDroidReasoningDefaults } from "./config-export/droid";
55
56
  export type { DroidModelEntry, DroidGeneratedConfig } from "./config-export/droid";
56
57
 
57
58
  import type { OpencodeLaunchEnv, OpencodeCatalogModel, ExportContext, PiModelEntry, ManagedContribution, ManagedFragment, ExportClientId, ExportClientSpec } from "./config-export/contracts";
58
59
  import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, PI_API_DIALECT, LOOPBACK_API_KEY_PLACEHOLDER, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV_REF, OPENCODE_API_KEY_ENV, HERMES_API_KEY_ENV, OPENCLAW_API_KEY_ENV, KILO_API_KEY_ENV } from "./config-export/constants";
59
- import { exportModelLabel, authoritativeContextWindow, outputBudgetFor, normalizeExportModels, inputModalitiesForClient, opencodeModelCapabilities, proxyAdmissionHeaders, singleFragment } from "./config-export/model-metadata";
60
+ import { exportModelLabel, authoritativeContextWindow, outputBudgetFor, inputBudgetFor, normalizeExportModels, inputModalitiesForClient, opencodeModelCapabilities, proxyAdmissionHeaders, singleFragment } from "./config-export/model-metadata";
61
+ import { exportReasoningEfforts, exportDefaultReasoningEffort, legacyReasoningMetadata, type LegacyEffortVariant } from "./config-export/reasoning-metadata";
60
62
  import { buildOmpClientConfig, summarizeOmp, buildOmpContribution } from "./config-export/omp";
61
- import { buildDshClientConfig, summarizeDsh, buildDshContribution } from "./config-export/dsh";
63
+ import { buildDshClientConfig, summarizeDsh, buildDshContribution, DSH_DESKTOP_PROFILE } from "./config-export/dsh";
62
64
  import { buildMcodeClientConfig, summarizeMcode, buildMcodeContribution } from "./config-export/mcode";
63
65
  import { buildZcodeClientConfig, summarizeZcode, buildZcodeContribution } from "./config-export/zcode";
64
66
  import { buildClineClientConfig, summarizeCline, buildClineContribution } from "./config-export/cline";
@@ -72,7 +74,7 @@ import { droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidCont
72
74
 
73
75
  export interface OpencodeModelEntry {
74
76
  name: string;
75
- limit?: { context: number; output: number };
77
+ limit?: { context: number; input?: number; output: number };
76
78
  /**
77
79
  * opencode's own capability fields, derived from the catalog row's declared input
78
80
  * modalities. Written only when the row declares at least one — an entry without them is
@@ -81,22 +83,30 @@ export interface OpencodeModelEntry {
81
83
  */
82
84
  attachment?: boolean;
83
85
  modalities?: { input: string[]; output: string[] };
86
+ tool_call?: boolean;
87
+ reasoning?: boolean;
88
+ interleaved?: { field: "reasoning_content" };
89
+ options?: { reasoningEffort: string };
90
+ variants?: Record<string, LegacyEffortVariant>;
84
91
  }
85
92
 
86
93
  /**
87
94
  * One selectable reasoning effort.
88
95
  *
89
- * opencode V2 applies these only from the `providers` block: a `variants` array under the
90
- * legacy `provider` block is parsed and then dropped, so the V1 block stays variant-free
91
- * rather than carrying fields that look configured but never reach a request.
96
+ * Native V2 variants use settings; legacy clients receive a separate options map.
92
97
  */
93
98
  export interface OpencodeModelVariant {
94
99
  id: string;
95
100
  settings: { reasoningEffort: string };
96
101
  }
97
102
 
98
- export interface OpencodeV2ModelEntry extends OpencodeModelEntry {
99
- variants?: OpencodeModelVariant[];
103
+ export interface OpencodeV2ModelEntry {
104
+ name: string;
105
+ limit?: { context: number; input?: number; output: number };
106
+ capabilities?: { tools: boolean; input?: string[]; output?: string[] };
107
+ compatibility?: { reasoningField: "reasoning_content" };
108
+ settings?: { reasoningEffort: string };
109
+ variants: OpencodeModelVariant[];
100
110
  }
101
111
 
102
112
  /** Endpoint and admission, spelled once and shared by both block generations. */
@@ -114,7 +124,7 @@ export interface OpencodeProviderBlock {
114
124
  models: Record<string, OpencodeModelEntry>;
115
125
  }
116
126
 
117
- /** opencode V2 provider block: `package` + `settings`. The only form whose variants apply. */
127
+ /** opencode V2 provider block: `package` + `settings` and native variant arrays. */
118
128
  export interface OpencodeV2ProviderBlock {
119
129
  package: string;
120
130
  name: string;
@@ -147,16 +157,10 @@ export interface OpencodeGeneratedConfig {
147
157
  const OPENCODE_PROVIDER_NPM = "@ai-sdk/openai-compatible";
148
158
 
149
159
  /**
150
- * opencode V2's spelling of the same runtime. V2 resolves providers through its own
151
- * package table and ignores the V1 `npm` field, so a V2 block has to name this package
152
- * or the provider is not loaded at all.
153
- *
154
- * Verified end-to-end against opencode 0.0.0-beta-18684: `GET /api/model` resolves this
155
- * package for the provider and applies the per-model `variants`. opencode changes its
156
- * provider package table between releases, so re-verify the supported range whenever it
157
- * moves; a stale string breaks only the V2 block, silently.
160
+ * Canonical V2 runtime spelling. Current clients alias the old @opencode-ai scope,
161
+ * but exported documents use the documented package rather than depending on that alias.
158
162
  */
159
- const OPENCODE_V2_PROVIDER_PACKAGE = "@opencode-ai/ai/providers/openai-compatible";
163
+ const OPENCODE_V2_PROVIDER_PACKAGE = "@opencode/ai/providers/openai-compatible";
160
164
 
161
165
  /** Display name for the provider block, identical in both generations. */
162
166
  const OPENCODE_PROVIDER_NAME = "OpenCodex";
@@ -432,6 +436,11 @@ export function dshConfigPath(env: OpencodeLaunchEnv = process.env, home: string
432
436
  return join(dshHomeDir(env, home), "settings.yaml");
433
437
  }
434
438
 
439
+ /** The Desktop profile's patch, where DSH 0.1.7+ reads provider routes (see `DSH_PROFILE_PROVIDER_PATH`). */
440
+ export function dshProfilePatchPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
441
+ return join(dshHomeDir(env, home), "profiles", DSH_DESKTOP_PROFILE, "cordis.patch.yml");
442
+ }
443
+
435
444
  /**
436
445
  * MiniMax Code stores runtime state under `MINIMAX_DATA_DIR`, then the legacy
437
446
  * `MAVIS_DATA_DIR`, and finally `~/.minimax`. Relative overrides are refused
@@ -672,35 +681,11 @@ function opencodeProviderConnection(baseURL: string, config: OcxConfig): Opencod
672
681
  }
673
682
 
674
683
  /**
675
- * Selectable reasoning efforts for one model, in canonical ladder order.
676
- *
677
- * No model-level `settings.reasoningEffort` default is emitted: the proxy already applies
678
- * its own configured default when a request carries no effort, and pinning one here would
679
- * override a default the user controls in opencodex. Variants are opt-in per selection,
680
- * which is the same reason we never emit `defaultModel` for MCode.
681
- *
682
- * `none` is dropped even when a ladder declares it.
683
- *
684
- * The original reason no longer holds and is recorded here so it is not repeated: the chat
685
- * ingress `OUTPUT_CONFIG_EFFORTS` allowlist DID omit `none`, so selecting it sent no effort
686
- * at all and fell back to the proxy default. That allowlist now accepts `none` (audit F7),
687
- * because it is the runtime's disable sentinel and dropping it let a provider default
688
- * re-enable thinking a caller had turned off.
689
- *
690
- * The variant stays filtered anyway, deliberately and narrowly: emitting it would change
691
- * what this exporter writes into a user's opencode config, and whether opencode's own
692
- * picker round-trips `reasoningEffort: "none"` to the wire this proxy reads has not been
693
- * verified here. Re-enabling it is a scoped follow-up that needs that check first, not a
694
- * side effect of an ingress fix. MCode and ZCode filter `none` for their own separate
695
- * reasons, documented at their call sites.
684
+ * Exact declared choices. An empty array is intentional: omitting variants lets OpenCode
685
+ * synthesize low/medium/high, even for a model whose upstream declares no adjustable effort.
696
686
  */
697
- function opencodeEffortVariants(model: OpencodeCatalogModel): OpencodeModelVariant[] | undefined {
698
- if (model.reasoningEfforts === undefined) return undefined;
699
- // Canonical order (none, minimal, then low..ultra) and dedupe, so the picker order does
700
- // not depend on whatever order a provider listed its efforts in.
701
- const efforts = canonicalizeReasoningEfforts(model.reasoningEfforts).filter(effort => effort !== "none");
702
- if (efforts.length === 0) return undefined;
703
- return efforts.map(effort => ({ id: effort, settings: { reasoningEffort: effort } }));
687
+ function opencodeEffortVariants(model: OpencodeCatalogModel): OpencodeModelVariant[] {
688
+ return (exportReasoningEfforts(model) ?? []).map(effort => ({ id: effort, settings: { reasoningEffort: effort } }));
704
689
  }
705
690
 
706
691
  /**
@@ -712,7 +697,7 @@ function opencodeEffortVariants(model: OpencodeCatalogModel): OpencodeModelVaria
712
697
  * the pair) clamped to the context window.
713
698
  *
714
699
  * Two blocks instead of one because opencode V2 reads the `providers` map and V1 reads
715
- * `provider`, and only the V2 form applies `variants`. Emitting both keeps V1 installs
700
+ * `provider`, with native arrays versus legacy maps for variants. Emitting both keeps V1 installs
716
701
  * working: V2 merges them by provider id and model id, so a model listed in both blocks
717
702
  * appears once, with the V2 entry's name, connection, and variants.
718
703
  */
@@ -725,36 +710,37 @@ export function opencodeProviderBlocks(
725
710
  const v2Models: Record<string, OpencodeV2ModelEntry> = {};
726
711
  for (const model of expandFastExportModels(catalogModels)) {
727
712
  const key = model.namespaced;
728
- const entry: OpencodeModelEntry = { name: exportModelLabel(model) };
713
+ const entry: OpencodeModelEntry = { name: exportModelLabel(model), ...legacyReasoningMetadata(model) };
729
714
  const context = authoritativeContextWindow(model.contextWindow);
730
715
  if (context !== undefined) {
731
716
  entry.limit = { context, output: outputBudgetFor(context, model) };
717
+ const input = inputBudgetFor(context, model);
718
+ if (input !== undefined) entry.limit.input = input;
732
719
  }
733
- // `attachment` / `modalities` are fields of opencode's V1 model schema — the shape its
734
- // published config.json defines and the one its loader reads (verified against opencode
735
- // 1.18.30, src/provider/provider.ts: `model.attachment ?? …` / `model.modalities?.input`).
736
- // They ride on both generations anyway: the two blocks are two spellings of one model list,
737
- // and the V2 model schema (capabilities.{tools,input,output}, which opencode fills by
738
- // migrating this same `modalities` field) ignores keys it does not define — its loader
739
- // decodes with `onExcessProperty: "ignore"`. Same values on both, so a merge cannot make
740
- // the two entries disagree.
720
+ // Native V2 decoders ignore legacy attachment/modalities fields. Emit each generation's
721
+ // actual capability shape rather than relying on migration of the other provider block.
741
722
  const capabilities = opencodeModelCapabilities(model.inputModalities);
742
723
  if (capabilities) {
743
724
  entry.attachment = capabilities.attachment;
744
725
  entry.modalities = capabilities.modalities;
745
726
  }
727
+ if (typeof model.supportsTools === "boolean") entry.tool_call = model.supportsTools;
746
728
  v1Models[key] = entry;
747
- const variants = opencodeEffortVariants(model);
748
- // Own `limit` and `modalities` objects, not shared references: the two blocks are
749
- // serialized and reasoned about separately, and an in-place edit of one must never move
750
- // the other.
729
+ const defaultEffort = exportDefaultReasoningEffort(model);
730
+ // V2 requires tools whenever capabilities exists. A partial object discards the entire
731
+ // native block, losing explicit variants. Unknown tools use the legacy modality migration
732
+ // rather than inventing a tools flag to make the native schema accept the declaration.
733
+ const v2Capabilities: OpencodeV2ModelEntry["capabilities"] = typeof model.supportsTools === "boolean" ? {
734
+ tools: model.supportsTools,
735
+ ...(capabilities ? { input: [...capabilities.modalities.input], output: [...capabilities.modalities.output] } : {}),
736
+ } : undefined;
751
737
  v2Models[key] = {
752
- ...entry,
738
+ name: entry.name,
753
739
  ...(entry.limit ? { limit: { ...entry.limit } } : {}),
754
- ...(entry.modalities
755
- ? { modalities: { input: [...entry.modalities.input], output: [...entry.modalities.output] } }
756
- : {}),
757
- ...(variants ? { variants } : {}),
740
+ ...(v2Capabilities ? { capabilities: v2Capabilities } : {}),
741
+ ...(entry.interleaved ? { compatibility: { reasoningField: "reasoning_content" } } : {}),
742
+ ...(defaultEffort !== undefined ? { settings: { reasoningEffort: defaultEffort } } : {}),
743
+ variants: opencodeEffortVariants(model),
758
744
  };
759
745
  }
760
746
  return {
@@ -6,7 +6,7 @@
6
6
  * dashboard or CLI pick, never creates the file, and refuses a file with comments: the write
7
7
  * re-serializes JSON, and a comment the user wrote would be lost without a word.
8
8
  */
9
- import { lstatSync, readFileSync } from "node:fs";
9
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { atomicWriteFileNoFollowUnclaimed } from "../config/atomic-write";
@@ -40,12 +40,26 @@ type Loaded =
40
40
 
41
41
  function load(path: string): Loaded {
42
42
  let text: string;
43
+ let fd: number | undefined;
43
44
  try {
44
- if (!lstatSync(path).isFile()) return { kind: "invalid" };
45
- text = readFileSync(path, "utf8");
45
+ // POSIX FIFOs must not block before fstat can reject them. Windows omits
46
+ // these flags and retains the descriptor/path identity checks below.
47
+ const flags = process.platform === "win32"
48
+ ? constants.O_RDONLY
49
+ : constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;
50
+ fd = openSync(path, flags);
51
+ const opened = fstatSync(fd, { bigint: true });
52
+ const current = lstatSync(path, { bigint: true });
53
+ if (!opened.isFile() || !current.isFile() || opened.dev !== current.dev || opened.ino !== current.ino) {
54
+ return { kind: "invalid" };
55
+ }
56
+ text = readFileSync(fd, "utf8");
46
57
  } catch (error) {
47
58
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return { kind: "absent" };
59
+ if ((error as NodeJS.ErrnoException).code === "ELOOP") return { kind: "invalid" };
48
60
  throw error;
61
+ } finally {
62
+ if (fd !== undefined) closeSync(fd);
49
63
  }
50
64
  if (hasJsoncComments(text)) return { kind: "comments" };
51
65
  try {