@vantran-se/openchamber-web 1.24.4 → 2.0.2

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 (367) hide show
  1. package/README.md +2 -8
  2. package/bin/lib/commands-connect-url.js +3 -8
  3. package/bin/lib/commands-tunnel.js +3 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-B82Oye3V.js} +1 -1
  5. package/dist/assets/DiffView-DJLmWn6Z.js +4 -0
  6. package/dist/assets/FilesView-DjksmZRo.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-DiUsUIgr.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-CYwWYLr3.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-CmctyRI4.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-DOHHcbgJ.js} +1 -1
  11. package/dist/assets/LinearIssuesView-DaqnylcV.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-HJNvWt7L.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-nDZRGg2I.js +12 -0
  14. package/dist/assets/OnboardingScreen-C2B5Bsof.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-C4FLBjpt.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-C_1S9c9n.js} +1 -1
  17. package/dist/assets/PlanView-_eZvb-G5.js +5 -0
  18. package/dist/assets/PluginPane-sM8fcZIV.js +3 -0
  19. package/dist/assets/SettingsView-BRfr46hb.js +29 -0
  20. package/dist/assets/SettingsWindow-CcUDwQHg.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-CrN1ZGwl.js +1 -0
  22. package/dist/assets/ToolOutputDialog-Cij4jI09.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-BXzwQ-JY.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-DSWTLJNH.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-CpUUgY8H.js} +2 -2
  26. package/dist/assets/appThemeRegistry-B5tHTkR8.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-D7D8qV8p.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-cq9uQ30i.js} +1 -1
  29. package/dist/assets/de-CUO1-KqF.js +10 -0
  30. package/dist/assets/es-BlbC6sGJ.js +16 -0
  31. package/dist/assets/fr-D2nwe_0W.js +14 -0
  32. package/dist/assets/index-BQOoYIOu.js +1 -0
  33. package/dist/assets/index-CLcM8HKx.css +1 -0
  34. package/dist/assets/index-CvLgnuNI.js +41 -0
  35. package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D0obJ2p8.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-DB3_rjyW.js} +1 -1
  37. package/dist/assets/ja-CA7vnDsZ.js +16 -0
  38. package/dist/assets/ko-Chy6a9l-.js +16 -0
  39. package/dist/assets/main-B9JFK3vF.js +896 -0
  40. package/dist/assets/main-Bhiqnrhd.js +2 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-tfqOFaBd.js} +4 -4
  42. package/dist/assets/miniChat-B0UOL5zA.js +2 -0
  43. package/dist/assets/mobile-BJppFIEo.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-Db6KuwCP.js} +3 -3
  45. package/dist/assets/pl-CTR5A_1P.js +16 -0
  46. package/dist/assets/pt-BR-BE1O1KEx.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-Yz5sfoFh.js +2 -0
  48. package/dist/assets/renderMobileApp-QQ-yB8aY.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-DKDdzRzm.js +27 -0
  50. package/dist/assets/scheduledTasksApi-C5sCuNqf.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-xURoo5t_.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-BZLGuYTU.js} +1 -1
  53. package/dist/assets/tr-Bh5vK6cW.js +7 -0
  54. package/dist/assets/uk-CLDkZFy_.js +16 -0
  55. package/dist/assets/useAppFontEffects-BxrtCnfR.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CeXuR-SR.js} +2 -2
  57. package/dist/assets/useRootScrollLock-DgWxezc1.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-DOuaQUZ0.js} +2 -2
  59. package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
  60. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
  61. package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
  62. package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
  63. package/dist/assets/{web-components-DxX8bZ1o.js → web-components-63H1h21j.js} +2 -2
  64. package/dist/assets/zh-CN-DODgcmYj.js +16 -0
  65. package/dist/assets/zh-TW-ByNfIwLA.js +16 -0
  66. package/dist/index.html +13 -7
  67. package/dist/mini-chat.html +5 -5
  68. package/dist/mobile.html +5 -5
  69. package/package.json +6 -5
  70. package/server/index.d.ts +1 -0
  71. package/server/index.js +246 -91
  72. package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
  73. package/server/lib/agent-tool/runtime.js +135 -54
  74. package/server/lib/agent-tool/runtime.test.js +155 -87
  75. package/server/lib/browser-control/DOCUMENTATION.md +37 -2
  76. package/server/lib/browser-control/provider.js +217 -0
  77. package/server/lib/browser-control/provider.test.js +251 -0
  78. package/server/lib/context-obligatory/runtime.js +61 -50
  79. package/server/lib/context-obligatory/runtime.test.js +62 -162
  80. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  81. package/server/lib/event-stream/delta-coalescer.js +0 -0
  82. package/server/lib/event-stream/delta-coalescer.test.js +68 -57
  83. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  84. package/server/lib/event-stream/global-hub.js +10 -1
  85. package/server/lib/event-stream/global-hub.test.js +27 -26
  86. package/server/lib/event-stream/index.js +6 -0
  87. package/server/lib/event-stream/protocol.js +26 -19
  88. package/server/lib/event-stream/protocol.test.js +18 -7
  89. package/server/lib/event-stream/rebind.test.js +5 -5
  90. package/server/lib/event-stream/runtime.test.js +17 -15
  91. package/server/lib/event-stream/translate-v2.js +313 -0
  92. package/server/lib/event-stream/translate-v2.test.js +209 -0
  93. package/server/lib/event-stream/upstream-reader.test.js +16 -16
  94. package/server/lib/fs/DOCUMENTATION.md +4 -2
  95. package/server/lib/fs/byte-range.js +35 -0
  96. package/server/lib/fs/byte-range.test.js +31 -0
  97. package/server/lib/fs/routes.js +47 -42
  98. package/server/lib/fs/routes.test.js +123 -18
  99. package/server/lib/git/service.js +12 -0
  100. package/server/lib/github/DOCUMENTATION.md +1 -1
  101. package/server/lib/github/pr-status.js +22 -2
  102. package/server/lib/github/pr-status.test.js +27 -14
  103. package/server/lib/guests/DOCUMENTATION.md +13 -0
  104. package/server/lib/guests/catalog.js +12 -0
  105. package/server/lib/guests/catalog.test.js +33 -0
  106. package/server/lib/guests/routes.js +10 -1
  107. package/server/lib/guests/service.js +192 -14
  108. package/server/lib/guests/service.test.js +201 -1
  109. package/server/lib/guests/surface.js +630 -0
  110. package/server/lib/guests/surface.test.js +486 -0
  111. package/server/lib/markdown-image-grants/routes.js +18 -5
  112. package/server/lib/markdown-image-grants/routes.test.js +9 -6
  113. package/server/lib/message-queue/DOCUMENTATION.md +33 -22
  114. package/server/lib/message-queue/runtime.js +155 -100
  115. package/server/lib/message-queue/runtime.test.js +168 -82
  116. package/server/lib/notifications/DOCUMENTATION.md +28 -0
  117. package/server/lib/notifications/control-stream.http.test.js +68 -0
  118. package/server/lib/notifications/emitter-runtime.js +15 -0
  119. package/server/lib/notifications/emitter-runtime.test.js +32 -0
  120. package/server/lib/notifications/push-runtime.js +5 -1
  121. package/server/lib/notifications/push-runtime.test.js +2 -2
  122. package/server/lib/notifications/routes.js +1 -1
  123. package/server/lib/notifications/runtime.js +115 -26
  124. package/server/lib/notifications/template-runtime.js +13 -15
  125. package/server/lib/notifications/template-runtime.test.js +16 -9
  126. package/server/lib/notifications/trigger-runtime.test.js +187 -0
  127. package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
  128. package/server/lib/openchamber-control/actions.js +3 -2
  129. package/server/lib/openchamber-control/error.js +1 -1
  130. package/server/lib/openchamber-control/file-open.js +59 -0
  131. package/server/lib/openchamber-control/file-open.test.js +57 -0
  132. package/server/lib/openchamber-control/routes.test.js +12 -0
  133. package/server/lib/openchamber-control/service.js +102 -49
  134. package/server/lib/openchamber-control/service.test.js +84 -44
  135. package/server/lib/openchamber-sessions/archive-store.js +224 -0
  136. package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
  137. package/server/lib/openchamber-sessions/opencode-client.js +22 -0
  138. package/server/lib/openchamber-sessions/routes.js +398 -328
  139. package/server/lib/openchamber-sessions/routes.test.js +777 -740
  140. package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
  141. package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
  142. package/server/lib/opencode/DOCUMENTATION.md +449 -29
  143. package/server/lib/opencode/agents.js +233 -327
  144. package/server/lib/opencode/auth.js +41 -44
  145. package/server/lib/opencode/auth.test.js +92 -0
  146. package/server/lib/opencode/cli-upgrade.d.ts +4 -0
  147. package/server/lib/opencode/cli-upgrade.js +18 -0
  148. package/server/lib/opencode/cli-upgrade.test.js +36 -0
  149. package/server/lib/opencode/commands.js +164 -145
  150. package/server/lib/opencode/compatibility.d.ts +16 -0
  151. package/server/lib/opencode/compatibility.js +88 -0
  152. package/server/lib/opencode/compatibility.test.js +62 -0
  153. package/server/lib/opencode/config-entity-routes.js +58 -29
  154. package/server/lib/opencode/config-entity-v2.test.js +290 -0
  155. package/server/lib/opencode/config-mutation-response.js +17 -7
  156. package/server/lib/opencode/config-mutation-response.test.js +4 -7
  157. package/server/lib/opencode/config-paths.test.js +2 -2
  158. package/server/lib/opencode/config-v2.d.ts +183 -0
  159. package/server/lib/opencode/config-v2.js +801 -0
  160. package/server/lib/opencode/config-v2.test.js +466 -0
  161. package/server/lib/opencode/credential-db.js +144 -0
  162. package/server/lib/opencode/credential-db.test.js +104 -0
  163. package/server/lib/opencode/env-runtime.js +48 -9
  164. package/server/lib/opencode/env-runtime.test.js +45 -0
  165. package/server/lib/opencode/feature-routes-runtime.js +15 -5
  166. package/server/lib/opencode/lifecycle-process.test.js +7 -4
  167. package/server/lib/opencode/lifecycle.js +85 -30
  168. package/server/lib/opencode/lifecycle.test.js +80 -8
  169. package/server/lib/opencode/login-shell-env.js +27 -0
  170. package/server/lib/opencode/managed-config-file.js +119 -0
  171. package/server/lib/opencode/managed-config-file.test.js +152 -0
  172. package/server/lib/opencode/managed-plugin-config.js +33 -13
  173. package/server/lib/opencode/mcp.js +43 -149
  174. package/server/lib/opencode/network-runtime.js +6 -7
  175. package/server/lib/opencode/openchamber-routes.test.js +2 -5
  176. package/server/lib/opencode/plugin-routes.js +3 -3
  177. package/server/lib/opencode/plugin-routes.test.js +6 -24
  178. package/server/lib/opencode/plugins.js +91 -40
  179. package/server/lib/opencode/plugins.test.js +28 -15
  180. package/server/lib/opencode/providers.js +63 -83
  181. package/server/lib/opencode/providers.test.js +128 -59
  182. package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
  183. package/server/lib/opencode/proxy.js +179 -129
  184. package/server/lib/opencode/pwa-manifest-routes.js +4 -2
  185. package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
  186. package/server/lib/opencode/response-envelope.js +14 -0
  187. package/server/lib/opencode/response-envelope.test.js +23 -0
  188. package/server/lib/opencode/routes-upgrade.test.js +73 -116
  189. package/server/lib/opencode/routes.js +121 -428
  190. package/server/lib/opencode/server-utils-runtime.js +10 -4
  191. package/server/lib/opencode/session-activity.js +78 -0
  192. package/server/lib/opencode/session-runtime.js +21 -10
  193. package/server/lib/opencode/session-runtime.test.js +9 -9
  194. package/server/lib/opencode/settings-helpers.js +9 -9
  195. package/server/lib/opencode/settings-helpers.test.js +7 -12
  196. package/server/lib/opencode/settings-registry.json +6 -8
  197. package/server/lib/opencode/settings-runtime.js +18 -1
  198. package/server/lib/opencode/settings-runtime.test.js +31 -2
  199. package/server/lib/opencode/shared.js +54 -22
  200. package/server/lib/opencode/shared.test.js +8 -4
  201. package/server/lib/opencode/shutdown-runtime.js +54 -7
  202. package/server/lib/opencode/shutdown-runtime.test.js +224 -30
  203. package/server/lib/opencode/skill-routes.js +23 -24
  204. package/server/lib/opencode/skills.js +7 -5
  205. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
  206. package/server/lib/opencode/upgrade-capability.test.js +1 -1
  207. package/server/lib/opencode/v1-migration-topup.js +288 -0
  208. package/server/lib/opencode/v1-migration-topup.test.js +266 -0
  209. package/server/lib/opencode/v2-install.d.ts +8 -0
  210. package/server/lib/opencode/v2-install.js +158 -0
  211. package/server/lib/opencode/v2-install.test.js +124 -0
  212. package/server/lib/opencode/watcher.js +10 -3
  213. package/server/lib/opencode/watcher.test.js +17 -21
  214. package/server/lib/opencode/websearch-config.js +66 -0
  215. package/server/lib/package-manager.js +55 -68
  216. package/server/lib/package-manager.test.js +241 -75
  217. package/server/lib/permission-auto-accept/runtime.js +38 -22
  218. package/server/lib/permission-auto-accept/runtime.test.js +39 -17
  219. package/server/lib/quota/DOCUMENTATION.md +4 -0
  220. package/server/lib/quota/providers/xai.js +4 -4
  221. package/server/lib/relay/tunnel-host.js +4 -1
  222. package/server/lib/relay/tunnel-host.test.js +7 -0
  223. package/server/lib/routing/DOCUMENTATION.md +52 -21
  224. package/server/lib/routing/defaults.js +23 -6
  225. package/server/lib/routing/jev.js +17 -7
  226. package/server/lib/routing/routes.http.test.js +68 -33
  227. package/server/lib/routing/routes.js +65 -22
  228. package/server/lib/routing/runtime.js +145 -48
  229. package/server/lib/routing/runtime.test.js +79 -55
  230. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
  231. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
  232. package/server/lib/scheduled-tasks/runtime.js +81 -74
  233. package/server/lib/scheduled-tasks/runtime.test.js +53 -1
  234. package/server/lib/session-assist/DOCUMENTATION.md +8 -3
  235. package/server/lib/session-assist/context.js +122 -46
  236. package/server/lib/session-assist/context.test.js +117 -49
  237. package/server/lib/session-assist/runtime.js +52 -22
  238. package/server/lib/session-assist/runtime.test.js +145 -173
  239. package/server/lib/session-goal/DOCUMENTATION.md +23 -15
  240. package/server/lib/session-goal/create.js +9 -12
  241. package/server/lib/session-goal/create.test.js +27 -28
  242. package/server/lib/session-goal/runtime.js +164 -77
  243. package/server/lib/session-goal/runtime.test.js +271 -519
  244. package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
  245. package/server/lib/session-knowledge/runtime.js +59 -41
  246. package/server/lib/session-knowledge/runtime.test.js +90 -20
  247. package/server/lib/small-model/DOCUMENTATION.md +154 -208
  248. package/server/lib/small-model/client.js +113 -0
  249. package/server/lib/small-model/index.js +296 -158
  250. package/server/lib/small-model/index.test.js +423 -256
  251. package/server/lib/small-model/routes.js +1 -1
  252. package/server/lib/spaces/DOCUMENTATION.md +807 -0
  253. package/server/lib/spaces/code-in-bait.js +185 -0
  254. package/server/lib/spaces/code-in.js +641 -0
  255. package/server/lib/spaces/code-in.test.js +1048 -0
  256. package/server/lib/spaces/errors.js +8 -0
  257. package/server/lib/spaces/exec-http.js +94 -0
  258. package/server/lib/spaces/gatekeeper-channel.js +202 -0
  259. package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
  260. package/server/lib/spaces/gatekeeper-program.cjs +914 -0
  261. package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
  262. package/server/lib/spaces/hardening.js +457 -0
  263. package/server/lib/spaces/hardening.test.js +368 -0
  264. package/server/lib/spaces/host-git.js +127 -0
  265. package/server/lib/spaces/host-git.test.js +137 -0
  266. package/server/lib/spaces/labels.js +234 -0
  267. package/server/lib/spaces/labels.test.js +219 -0
  268. package/server/lib/spaces/layout.js +184 -0
  269. package/server/lib/spaces/layout.test.js +30 -0
  270. package/server/lib/spaces/manager.js +82 -0
  271. package/server/lib/spaces/manager.test.js +114 -0
  272. package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
  273. package/server/lib/spaces/places/contract-suite.js +156 -0
  274. package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
  275. package/server/lib/spaces/places/contract.test.js +6 -0
  276. package/server/lib/spaces/places/docker-engine.js +126 -0
  277. package/server/lib/spaces/places/docker-live-support.js +202 -0
  278. package/server/lib/spaces/places/docker-tools.js +186 -0
  279. package/server/lib/spaces/places/docker.js +587 -0
  280. package/server/lib/spaces/places/docker.test.js +1610 -0
  281. package/server/lib/spaces/places/escape-suite.js +1018 -0
  282. package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
  283. package/server/lib/spaces/places/fake-docker.js +375 -0
  284. package/server/lib/spaces/places/memory-place.js +103 -0
  285. package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
  286. package/server/lib/spaces/places/registry.js +43 -0
  287. package/server/lib/spaces/places/registry.test.js +50 -0
  288. package/server/lib/spaces/places/server.docker.live.test.js +225 -0
  289. package/server/lib/spaces/run-command.js +261 -0
  290. package/server/lib/spaces/run-command.test.js +396 -0
  291. package/server/lib/spaces/space-server.js +190 -0
  292. package/server/lib/spaces/space-server.test.js +335 -0
  293. package/server/lib/spaces/tools-filler.js +82 -0
  294. package/server/lib/spaces/tools-filler.test.js +148 -0
  295. package/server/lib/spaces/tools-pack.js +49 -0
  296. package/server/lib/spaces/tools-pack.test.js +57 -0
  297. package/server/lib/spaces/tools.js +138 -0
  298. package/server/lib/spaces/tools.test.js +153 -0
  299. package/server/lib/tts/openai-sdk.js +11 -0
  300. package/server/lib/tts/service.js +7 -5
  301. package/server/lib/tts/stt.js +3 -2
  302. package/server/lib/tunnels/DOCUMENTATION.md +2 -0
  303. package/server/lib/tunnels/routes.js +7 -0
  304. package/server/lib/tunnels/routes.test.js +55 -0
  305. package/server/lib/ui-auth/ui-auth.js +38 -2
  306. package/server/lib/ui-auth/ui-auth.test.js +42 -0
  307. package/server/lib/ui-auth/ui-passkeys.js +12 -10
  308. package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
  309. package/server/lib/walkthrough/pull-request.js +61 -8
  310. package/server/lib/walkthrough/pull-request.test.js +54 -1
  311. package/server/lib/walkthrough/routes.js +20 -0
  312. package/server/lib/walkthrough/routes.test.js +19 -0
  313. package/server/opencode-proxy.test.js +190 -166
  314. package/server/opencode-worktree-readiness.test.js +5 -4
  315. package/dist/assets/DiffView-B2CD9wC8.js +0 -4
  316. package/dist/assets/FilesView-C4vMFclc.js +0 -13
  317. package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
  320. package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
  321. package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
  322. package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
  323. package/dist/assets/SettingsView-CSinuH-1.js +0 -29
  324. package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
  326. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
  328. package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
  329. package/dist/assets/de-Bro6_zyl.js +0 -10
  330. package/dist/assets/es-Bb9XQZvd.js +0 -16
  331. package/dist/assets/fr-Dgfzn-IL.js +0 -14
  332. package/dist/assets/index-CtQNmqUL.js +0 -1
  333. package/dist/assets/index-DqaMWNrC.css +0 -1
  334. package/dist/assets/index-Dqc6USl3.js +0 -40
  335. package/dist/assets/ja-BYsakGFh.js +0 -16
  336. package/dist/assets/ko-BSaQz9aN.js +0 -16
  337. package/dist/assets/main-B62_pjL5.js +0 -2
  338. package/dist/assets/main-BEdcOI2P.js +0 -896
  339. package/dist/assets/miniChat-LT8Rqofr.js +0 -2
  340. package/dist/assets/mobile-CqarEynh.js +0 -2
  341. package/dist/assets/pl-OIviPdoj.js +0 -16
  342. package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
  344. package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
  346. package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
  347. package/dist/assets/tr-CQI-KPre.js +0 -7
  348. package/dist/assets/uk-CqW7R4DA.js +0 -16
  349. package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
  350. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
  351. package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
  352. package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
  353. package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
  354. package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
  355. package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
  356. package/server/lib/routing/feature-flag.js +0 -11
  357. package/server/lib/small-model/call.js +0 -851
  358. package/server/lib/small-model/call.test.js +0 -1292
  359. package/server/lib/small-model/catalog.js +0 -13
  360. package/server/lib/small-model/resolve.js +0 -131
  361. package/server/lib/small-model/resolve.test.js +0 -197
  362. package/server/lib/small-model/runtime-providers.js +0 -164
  363. package/server/lib/small-model/runtime-providers.test.js +0 -118
  364. package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
  365. package/server/lib/system-prompt/runtime.js +0 -49
  366. package/server/lib/system-prompt/runtime.test.js +0 -76
  367. package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
@@ -1,13 +1,15 @@
1
1
  import fs from 'fs';
2
+ import { setTimeout as delay } from 'node:timers/promises';
2
3
  import os from 'os';
3
4
  import path from 'path';
4
- import { readAuthFile } from '../opencode/auth.js';
5
- import { readConfigLayers } from '../opencode/shared.js';
6
- import { getModelCatalog } from './catalog.js';
7
- import { resolveSmallModel, parseModelRef, isUsableAuthEntry, getAuthEntryForProvider } from './resolve.js';
8
- import { DEDICATED_WIRE_FORMAT_PROVIDERS, callSmallModel, resolveProviderLogin } from './call.js';
9
5
  import { readMergedSettingsSync } from '../opencode/settings-files.js';
10
- import { getRuntimeProviderSnapshot } from './runtime-providers.js';
6
+ import {
7
+ findModelInfo,
8
+ getDefaultModelInfo,
9
+ getSmallModelClient,
10
+ listModelInfos,
11
+ listProviderInfos,
12
+ } from './client.js';
11
13
 
12
14
  // Never a small model, whatever the transport looks like. A plugin can publish
13
15
  // an OpenAI-compatible endpoint for Claude Code, but it is a façade over the
@@ -17,6 +19,8 @@ import { getRuntimeProviderSnapshot } from './runtime-providers.js';
17
19
  // conditional on an endpoint existing.
18
20
  const CLAUDE_CODE_PROVIDER = 'claude-code';
19
21
 
22
+ const DEFAULT_TIMEOUT_MS = 60_000;
23
+
20
24
  const OPENCHAMBER_SETTINGS_FILE = path.join(
21
25
  process.env.OPENCHAMBER_DATA_DIR
22
26
  ? path.resolve(process.env.OPENCHAMBER_DATA_DIR)
@@ -25,7 +29,7 @@ const OPENCHAMBER_SETTINGS_FILE = path.join(
25
29
  );
26
30
 
27
31
  // OpenChamber's own settings: when the user unchecks "use default small model"
28
- // their explicit override outranks every other resolution step.
32
+ // their explicit override outranks the model OpenCode would pick.
29
33
  const readSmallModelSettingsOverride = () => {
30
34
  const settings = readMergedSettingsSync({ fs, path, settingsFilePath: OPENCHAMBER_SETTINGS_FILE });
31
35
  if (settings.smallModelUseDefault !== false) return null;
@@ -33,10 +37,20 @@ const readSmallModelSettingsOverride = () => {
33
37
  return override || null;
34
38
  };
35
39
 
40
+ export function parseModelRef(value) {
41
+ if (typeof value !== 'string') return null;
42
+ const trimmed = value.trim();
43
+ const slash = trimmed.indexOf('/');
44
+ if (slash <= 0 || slash === trimmed.length - 1) return null;
45
+ return {
46
+ providerID: trimmed.slice(0, slash),
47
+ modelID: trimmed.slice(slash + 1),
48
+ };
49
+ }
50
+
36
51
  // Rough safety clamp so a huge input never blows the model's context window.
37
- // Token estimate is ~4 chars/token; when the catalog has no limit for the
38
- // model (Copilot/codex utility models are not listed) a conservative default
39
- // applies.
52
+ // Token estimate is ~4 chars/token; when OpenCode reports no limit for the
53
+ // model a conservative default applies.
40
54
  const DEFAULT_CONTEXT_TOKENS = 64_000;
41
55
  const OUTPUT_RESERVE_TOKENS = 4_000;
42
56
 
@@ -45,10 +59,10 @@ const OUTPUT_RESERVE_TOKENS = 4_000;
45
59
  * to leave for the answer. The reserve must match the output budget the caller
46
60
  * will actually request, or the two disagree and the model overruns its context.
47
61
  */
48
- export const getModelInputCharBudget = ({ catalog, providerID, modelID, outputReserveTokens }) => {
49
- const limit = catalog?.[providerID]?.models?.[modelID]?.limit;
50
- const known = Number(limit?.context) > 0;
51
- const contextTokens = known ? Number(limit.context) : DEFAULT_CONTEXT_TOKENS;
62
+ export const getModelInputCharBudget = ({ modelInfo, outputReserveTokens }) => {
63
+ const context = Number(modelInfo?.limit?.context);
64
+ const known = context > 0;
65
+ const contextTokens = known ? context : DEFAULT_CONTEXT_TOKENS;
52
66
  const reserve = Number(outputReserveTokens) > 0 ? Number(outputReserveTokens) : OUTPUT_RESERVE_TOKENS;
53
67
  const inputBudgetTokens = Math.max(1_000, contextTokens - reserve);
54
68
  return { maxChars: inputBudgetTokens * 4, contextTokens, contextKnown: known };
@@ -56,21 +70,24 @@ export const getModelInputCharBudget = ({ catalog, providerID, modelID, outputRe
56
70
 
57
71
  /**
58
72
  * The output budget to actually request: what the caller asked for, capped by
59
- * what the model admits it can emit. Asking for more than `limit.output` is
60
- * rejected outright by some providers and silently ignored by others.
73
+ * what the model admits it can emit.
74
+ *
75
+ * `/api/experimental/generate` takes no output budget of its own, so this number only
76
+ * shapes the input reserve — but it has to stay the same number on both sides
77
+ * or a caller that asks for a large answer overruns the context.
61
78
  */
62
- const resolveOutputTokens = ({ catalog, providerID, modelID, maxOutputTokens }) => {
79
+ const resolveOutputTokens = ({ modelInfo, maxOutputTokens }) => {
63
80
  const requested = Number(maxOutputTokens) > 0 ? Number(maxOutputTokens) : 0;
64
81
  if (!requested) return undefined;
65
- const limit = Number(catalog?.[providerID]?.models?.[modelID]?.limit?.output);
82
+ const limit = Number(modelInfo?.limit?.output);
66
83
  return limit > 0 ? Math.min(requested, limit) : requested;
67
84
  };
68
85
 
69
86
  // `truncate` keeps the historical behavior for callers whose prompt losing its
70
87
  // tail is survivable (summaries, commit messages). `error` is for callers whose
71
88
  // output would be quietly wrong on a clipped input — they need the failure.
72
- const clampPromptToModelLimit = ({ prompt, catalog, providerID, modelID, onOverflow, outputReserveTokens }) => {
73
- const { maxChars } = getModelInputCharBudget({ catalog, providerID, modelID, outputReserveTokens });
89
+ const clampPromptToModelLimit = ({ prompt, modelInfo, providerID, modelID, onOverflow, outputReserveTokens }) => {
90
+ const { maxChars } = getModelInputCharBudget({ modelInfo, outputReserveTokens });
74
91
  if (prompt.length <= maxChars) {
75
92
  return { prompt, truncated: false };
76
93
  }
@@ -83,43 +100,162 @@ const clampPromptToModelLimit = ({ prompt, catalog, providerID, modelID, onOverf
83
100
  return { prompt: `${prompt.slice(0, maxChars)}…`, truncated: true };
84
101
  };
85
102
 
86
- const readConfiguredSmallModel = (workingDirectory) => {
103
+ const noClientError = () => Object.assign(
104
+ new Error('No small model available — OpenCode is not reachable'),
105
+ { statusCode: 404 },
106
+ );
107
+
108
+ /**
109
+ * The model families that count as "small", most preferred first. The same
110
+ * list OpenCode uses for its own session titles (`Catalog.model.small` in
111
+ * `packages/core/src/catalog.ts`); OpenCode does not expose that lookup over
112
+ * HTTP, so the scan is repeated here on `GET /api/model`.
113
+ */
114
+ export const SMALL_MODEL_FAMILY_PRIORITY = ['gpt-luna', 'gemini-flash-lite', 'gemini-flash', 'claude-haiku', 'gpt-nano', 'gpt-mini'];
115
+ // The last two are not on OpenCode's list; v1 counted them as small and a
116
+ // provider with nothing else cheap (Copilot's utility models, for one)
117
+ // would otherwise fall through to the session's big model.
118
+
119
+ /**
120
+ * A model's family: the catalog's `family` (models.dev) when it has one,
121
+ * else read from the id. A custom provider or a subscription outside the
122
+ * catalog has no `family`, yet its `gemini-3.6-flash` is still a flash.
123
+ */
124
+ export const familyOf = (model) => {
125
+ if (model?.family) return String(model.family);
126
+ const id = String(model?.id ?? '').toLowerCase();
127
+ if (id.includes('luna')) return 'gpt-luna';
128
+ if (id.includes('flash-lite') || id.includes('flash_lite')) return 'gemini-flash-lite';
129
+ if (id.includes('flash')) return 'gemini-flash';
130
+ if (id.includes('haiku')) return 'claude-haiku';
131
+ if (id.includes('nano')) return 'gpt-nano';
132
+ if (id.includes('mini') && !id.includes('minimax')) return 'gpt-mini';
133
+ return null;
134
+ };
135
+
136
+ /**
137
+ * The small model within one provider: the newest enabled, active, text-in
138
+ * text-out model of the first family in `SMALL_MODEL_FAMILY_PRIORITY` that the
139
+ * provider has. Null when the provider has none of those families.
140
+ */
141
+ export const pickSmallModelInProvider = (models, providerID) => pickSmallModel(models, (model) => model.providerID === providerID);
142
+
143
+ /**
144
+ * The small model across every provider OpenCode can call: same family
145
+ * order, newest release first within a family. What v1 did after the
146
+ * session provider came up empty; for callers without a session (commit
147
+ * messages, spoken summaries) it is the first place to look.
148
+ */
149
+ export const pickSmallModelAnywhere = (models) => pickSmallModel(models, () => true);
150
+
151
+ const pickSmallModel = (models, accept) => {
152
+ const candidates = models
153
+ .filter((model) => model && accept(model)
154
+ && model.providerID !== CLAUDE_CODE_PROVIDER
155
+ && model.enabled !== false
156
+ && (model.status === undefined || model.status === 'active')
157
+ && (model.capabilities?.input ?? ['text']).some((item) => String(item).startsWith('text'))
158
+ && (model.capabilities?.output ?? ['text']).some((item) => String(item).startsWith('text')))
159
+ .sort((a, b) => (Number(b.time?.released) || 0) - (Number(a.time?.released) || 0));
160
+ for (const family of SMALL_MODEL_FAMILY_PRIORITY) {
161
+ const found = candidates.find((model) => familyOf(model) === family);
162
+ if (found) return { providerID: found.providerID, modelID: found.id };
163
+ }
164
+ return null;
165
+ };
166
+
167
+ /**
168
+ * Which model this call runs on, in order:
169
+ *
170
+ * 1. An explicit request model.
171
+ * 2. OpenChamber's settings override (Settings → Sessions → Small Model).
172
+ * 3. The small model of the session's provider (family scan above) —
173
+ * `session-provider-small`. A caller that must not leave that provider
174
+ * then falls back to the session's own model (`session-model`): costlier
175
+ * than a small model elsewhere, but never someone else's subscription.
176
+ * 4. The small model of any provider OpenCode can call, newest first —
177
+ * `small`.
178
+ * 5. `GET /api/model/default`: OpenCode's default model — `default`. This is
179
+ * the chat default, not a small model; OpenCode's own small-model chain is
180
+ * not reachable over HTTP, which is why steps 3 and 4 live here.
181
+ */
182
+ const resolveSmallModel = async ({ client, directory, model, preferredProviderID, preferredModelID, restrictToPreferredProvider }) => {
183
+ const explicit = parseModelRef(model);
184
+ if (explicit) return { ...explicit, source: 'request' };
185
+
186
+ const fromSettings = parseModelRef(readSmallModelSettingsOverride());
187
+ if (fromSettings) return { ...fromSettings, source: 'settings' };
188
+
189
+ const models = await listModelInfos(client, directory);
190
+ if (preferredProviderID) {
191
+ const small = pickSmallModelInProvider(models, preferredProviderID);
192
+ if (small) return { ...small, source: 'session-provider-small' };
193
+ }
194
+ if (restrictToPreferredProvider && preferredProviderID && preferredModelID) {
195
+ return { providerID: preferredProviderID, modelID: preferredModelID, source: 'session-model' };
196
+ }
197
+
198
+ const anywhere = pickSmallModelAnywhere(models);
199
+ if (anywhere) return { ...anywhere, source: 'small' };
200
+
201
+ const fallback = await getDefaultModelInfo(client);
202
+ if (!fallback) return null;
203
+ return { providerID: fallback.providerID, modelID: fallback.id, source: 'default' };
204
+ };
205
+
206
+ const JSON_FENCE = /^```(?:json)?\s*\n?([\s\S]*?)\n?```$/;
207
+
208
+ /**
209
+ * `/api/experimental/generate` has no structured-output mode, so the schema travels in the
210
+ * prompt and the reply is parsed here.
211
+ */
212
+ const buildSchemaInstruction = (responseSchema) =>
213
+ `Reply with JSON matching this schema and nothing else: ${JSON.stringify(responseSchema)}`;
214
+
215
+ const extractJsonText = (raw) => {
216
+ const trimmed = typeof raw === 'string' ? raw.trim() : '';
217
+ if (!trimmed) return null;
218
+ const fenced = JSON_FENCE.exec(trimmed);
219
+ const candidate = (fenced ? fenced[1] : trimmed).trim();
87
220
  try {
88
- const { mergedConfig } = readConfigLayers(workingDirectory);
89
- const value = mergedConfig?.small_model;
90
- return typeof value === 'string' ? value : null;
221
+ JSON.parse(candidate);
222
+ return candidate;
91
223
  } catch {
92
224
  return null;
93
225
  }
94
226
  };
95
227
 
228
+ const requestOptions = ({ timeoutMs, signal }) => {
229
+ const timeout = Number(timeoutMs) > 0 ? Number(timeoutMs) : DEFAULT_TIMEOUT_MS;
230
+ const signals = [AbortSignal.timeout(timeout)];
231
+ if (signal) signals.push(signal);
232
+ return { signal: AbortSignal.any(signals) };
233
+ };
234
+
96
235
  /**
97
- * Generates text with the user's small model, resolved and authenticated
98
- * entirely server-side from the OpenCode config and auth store.
236
+ * Generates text with the user's small model through the running OpenCode.
237
+ * Credentials stay inside OpenCode; this server only sends a prompt.
99
238
  */
100
- export async function generateSmallModelText({ prompt, system, maxOutputTokens, model, directory, sessionID, preferredProviderID, preferredModelID, restrictToPreferredProvider = false, responseSchema, timeoutMs, signal, onOverflow = 'truncate' }) {
239
+ export async function generateSmallModelText({ prompt, system, maxOutputTokens, model, directory, preferredProviderID, preferredModelID, restrictToPreferredProvider = false, responseSchema, timeoutMs, signal, onOverflow = 'truncate' }) {
101
240
  if (typeof prompt !== 'string' || !prompt.trim()) {
102
241
  throw Object.assign(new Error('prompt is required'), { statusCode: 400 });
103
242
  }
104
243
 
105
- const auth = readAuthFile();
106
- const catalog = await getModelCatalog().catch(() => ({}));
244
+ const client = getSmallModelClient(directory);
245
+ if (!client) throw noClientError();
107
246
 
108
- const explicit = parseModelRef(model);
109
- const resolved = explicit
110
- ? { ...explicit, source: 'request' }
111
- : resolveSmallModel({
112
- auth,
113
- catalog,
114
- settingsSmallModel: readSmallModelSettingsOverride(),
115
- configSmallModel: readConfiguredSmallModel(directory),
116
- preferredProviderID,
117
- preferredModelID,
118
- });
247
+ const resolved = await resolveSmallModel({
248
+ client,
249
+ directory,
250
+ model,
251
+ preferredProviderID,
252
+ preferredModelID,
253
+ restrictToPreferredProvider,
254
+ });
119
255
 
120
256
  if (!resolved) {
121
257
  throw Object.assign(
122
- new Error('No small model available — no authenticated provider has a suitable model'),
258
+ new Error('No small model available — OpenCode reports no default model'),
123
259
  { statusCode: 404 },
124
260
  );
125
261
  }
@@ -131,12 +267,11 @@ export async function generateSmallModelText({ prompt, system, maxOutputTokens,
131
267
  );
132
268
  }
133
269
 
134
- // Callers with a session context can forbid silently switching providers:
135
- // an explicit user choice (settings override, opencode config, request
136
- // model) is always allowed, anything else must stay on the session's
137
- // provider.
270
+ // A caller that must stay on its session's provider is only overruled by an
271
+ // explicit user choice (the settings override or a request model).
138
272
  if (restrictToPreferredProvider
139
- && !['settings', 'config', 'request'].includes(resolved.source)
273
+ && !['settings', 'request'].includes(resolved.source)
274
+ && preferredProviderID
140
275
  && resolved.providerID !== preferredProviderID) {
141
276
  throw Object.assign(
142
277
  new Error('No small model available within the session provider'),
@@ -144,36 +279,76 @@ export async function generateSmallModelText({ prompt, system, maxOutputTokens,
144
279
  );
145
280
  }
146
281
 
147
- const outputTokens = resolveOutputTokens({
148
- catalog,
149
- providerID: resolved.providerID,
150
- modelID: resolved.modelID,
151
- maxOutputTokens,
152
- });
282
+ const models = await listModelInfos(client, directory);
283
+ const modelInfo = findModelInfo(models, resolved.providerID, resolved.modelID);
284
+
285
+ const outputTokens = resolveOutputTokens({ modelInfo, maxOutputTokens });
153
286
 
154
287
  const clamped = clampPromptToModelLimit({
155
288
  prompt: prompt.trim(),
156
- catalog,
289
+ modelInfo,
157
290
  providerID: resolved.providerID,
158
291
  modelID: resolved.modelID,
159
292
  onOverflow,
160
293
  outputReserveTokens: outputTokens,
161
294
  });
162
295
 
163
- const text = await callSmallModel({
164
- auth,
165
- catalog,
166
- workingDirectory: directory,
167
- sessionID,
168
- providerID: resolved.providerID,
169
- modelID: resolved.modelID,
170
- prompt: clamped.prompt,
171
- system: typeof system === 'string' && system.trim() ? system.trim() : undefined,
172
- maxOutputTokens: outputTokens,
173
- responseSchema,
174
- timeoutMs,
175
- signal,
176
- });
296
+ // `/api/experimental/generate` takes a single prompt, so the system instructions lead it.
297
+ const sections = [];
298
+ if (typeof system === 'string' && system.trim()) sections.push(system.trim());
299
+ sections.push(clamped.prompt);
300
+ if (responseSchema) sections.push(buildSchemaInstruction(responseSchema));
301
+ const fullPrompt = sections.join('\n\n');
302
+
303
+ const generationOptions = requestOptions({ timeoutMs, signal });
304
+ const unavailableMessage = `Model unavailable: ${resolved.providerID}/${resolved.modelID}`;
305
+ let retriedUnavailable = false;
306
+ const send = async () => {
307
+ const result = await client.generate.text(
308
+ { prompt: fullPrompt, model: { id: resolved.modelID, providerID: resolved.providerID } },
309
+ generationOptions,
310
+ );
311
+ return typeof result?.text === 'string' ? result.text : '';
312
+ };
313
+
314
+ const sendWithCatalogRetry = async () => {
315
+ try {
316
+ return await send();
317
+ } catch (error) {
318
+ if (error?._tag !== 'InvalidRequestError' || error.message !== unavailableMessage) throw error;
319
+ // OpenCode 2 can resolve a cold catalog before its models arrive.
320
+ // This rejection precedes provider dispatch; other failures must not retry.
321
+ if (!retriedUnavailable) {
322
+ retriedUnavailable = true;
323
+ await delay(500, undefined, { signal: generationOptions.signal });
324
+ return sendWithCatalogRetry();
325
+ }
326
+ throw Object.assign(new Error(unavailableMessage), {
327
+ statusCode: 503,
328
+ code: 'small-model-unavailable',
329
+ });
330
+ }
331
+ };
332
+
333
+ let text = await sendWithCatalogRetry();
334
+
335
+ if (responseSchema) {
336
+ // One retry: a model that ignored the shape once often honours it on a
337
+ // second pass, and the alternative is failing a walkthrough over a stray
338
+ // sentence of preamble.
339
+ let json = extractJsonText(text);
340
+ if (json === null) {
341
+ text = await sendWithCatalogRetry();
342
+ json = extractJsonText(text);
343
+ }
344
+ if (json === null) {
345
+ throw Object.assign(
346
+ new Error(`${resolved.providerID}/${resolved.modelID} did not return JSON matching the requested schema`),
347
+ { statusCode: 422, code: 'structured-output-unsupported', providerID: resolved.providerID, modelID: resolved.modelID },
348
+ );
349
+ }
350
+ text = json;
351
+ }
177
352
 
178
353
  return {
179
354
  text: text.trim(),
@@ -185,29 +360,32 @@ export async function generateSmallModelText({ prompt, system, maxOutputTokens,
185
360
  }
186
361
 
187
362
  /**
188
- * Provider ids the small model can actually call — an auth.json login, or a
189
- * credential and endpoint the running OpenCode resolved for a plugin. Used by
190
- * the Small Model and Changes Walkthrough pickers to hide providers that would
191
- * only ever fail (e.g. opencode free models without a token).
363
+ * Provider ids the small model can actually call. A provider counts when
364
+ * OpenCode has at least one enabled model for it — that is the same test
365
+ * OpenCode applies before letting a chat turn use it.
366
+ *
367
+ * The provider list alone is not enough: it comes back empty on setups where
368
+ * models are perfectly usable, so the model list is the authority and the
369
+ * provider list only contributes names.
192
370
  */
193
371
  export async function listAuthenticatedProviders() {
372
+ const client = getSmallModelClient();
373
+ if (!client) return [];
194
374
  try {
195
- const auth = readAuthFile();
196
- const ids = new Set(
197
- Object.keys(auth || {}).filter((providerID) => isUsableAuthEntry(auth[providerID])),
198
- );
199
- // The catalog id is github-copilot while legacy auth entries may sit
200
- // under the copilot alias.
201
- if (isUsableAuthEntry(getAuthEntryForProvider(auth, 'github-copilot'))) {
202
- ids.add('github-copilot');
375
+ const [providers, models] = await Promise.all([
376
+ listProviderInfos(client),
377
+ listModelInfos(client),
378
+ ]);
379
+ const enabled = new Set();
380
+ for (const model of models) {
381
+ if (model?.enabled === false) continue;
382
+ if (typeof model?.providerID === 'string' && model.providerID) enabled.add(model.providerID);
203
383
  }
204
- // Kept separate so a runtime lookup that goes wrong costs the providers it
205
- // would have added, never the logins already established from disk.
206
- try {
207
- for (const providerID of await listRuntimeCallableProviders()) ids.add(providerID);
208
- } catch {
209
- // The auth.json set below stands on its own.
384
+ const ids = new Set();
385
+ for (const provider of providers) {
386
+ if (typeof provider?.id === 'string' && enabled.has(provider.id)) ids.add(provider.id);
210
387
  }
388
+ for (const id of enabled) ids.add(id);
211
389
  ids.delete(CLAUDE_CODE_PROVIDER);
212
390
  return Array.from(ids);
213
391
  } catch {
@@ -215,43 +393,6 @@ export async function listAuthenticatedProviders() {
215
393
  }
216
394
  }
217
395
 
218
- /**
219
- * Providers that only the running OpenCode knows about — plugin-registered
220
- * ones, and any whose endpoint is resolved at startup.
221
- *
222
- * The test is the same one applied to an auth.json login: a credential we may
223
- * use and somewhere to send it. Whether the endpoint answers the protocol we
224
- * speak is not knowable from any field OpenCode reports, and guessing it wrong
225
- * removes a working model from the picker with nothing to explain it.
226
- */
227
- async function listRuntimeCallableProviders() {
228
- const snapshot = await getRuntimeProviderSnapshot();
229
- if (!snapshot) return [];
230
- const ids = [];
231
- for (const id of snapshot.connected) {
232
- const provider = snapshot.providers.get(id);
233
- // No credential we may use — including the zen sentinel, whose free models
234
- // belong to OpenCode's own server.
235
- if (!provider?.apiKey || !provider.baseURL) continue;
236
- // Reached through a dedicated wire format and already covered by the
237
- // auth.json scan above.
238
- if (DEDICATED_WIRE_FORMAT_PROVIDERS.has(id)) continue;
239
- ids.push(id);
240
- }
241
- return ids;
242
- }
243
-
244
- /**
245
- * Reports which model would be used, without calling it.
246
- *
247
- * `inputCharBudget` and `structuredOutput` let callers refuse work before
248
- * spending a request: the walkthrough needs both a big enough context and
249
- * schema-shaped output, and would rather tell the user to pick another model
250
- * than send a doomed prompt. `structuredOutput` is deliberately tri-state —
251
- * the catalog omits the field for roughly half of all models (aggregators and
252
- * proxies especially), and treating "unknown" as "unsupported" would hide
253
- * models that work fine.
254
- */
255
396
  /**
256
397
  * The reserve, resolved against the model that was actually picked.
257
398
  *
@@ -264,48 +405,45 @@ const resolveReserveTokens = (outputReserveTokens, limits) => (
264
405
  typeof outputReserveTokens === 'function' ? outputReserveTokens(limits) : outputReserveTokens
265
406
  );
266
407
 
408
+ /**
409
+ * Reports which model would be used, without calling it.
410
+ *
411
+ * `structuredOutput` stays `null`: `/api/experimental/generate` has no structured-output
412
+ * mode for any model, and this module emulates it through the prompt. Callers
413
+ * must read `null` as "try it", which is exactly right here — the verdict
414
+ * comes from the reply, not from a capability flag.
415
+ */
267
416
  export async function describeSmallModel({ directory, preferredProviderID, preferredModelID, outputReserveTokens, overrideModel } = {}) {
268
- const auth = readAuthFile();
269
- const catalog = await getModelCatalog().catch(() => ({}));
417
+ const client = getSmallModelClient(directory);
418
+ if (!client) return null;
419
+
270
420
  // A caller with its own model setting (the diff walkthrough) outranks the
271
421
  // small-model chain entirely — it asked for this model on purpose.
272
- const explicit = parseModelRef(overrideModel);
273
- const resolved = explicit
274
- ? { ...explicit, source: 'request' }
275
- : resolveSmallModel({
276
- auth,
277
- catalog,
278
- settingsSmallModel: readSmallModelSettingsOverride(),
279
- configSmallModel: readConfiguredSmallModel(directory),
280
- preferredProviderID,
281
- preferredModelID,
282
- });
283
- if (!resolved) return resolved;
284
-
285
- const entry = catalog?.[resolved.providerID]?.models?.[resolved.modelID];
286
- const outputTokenLimit = Number(entry?.limit?.output) > 0 ? Number(entry.limit.output) : null;
422
+ const resolved = await resolveSmallModel({
423
+ client,
424
+ directory,
425
+ model: overrideModel,
426
+ preferredProviderID,
427
+ preferredModelID,
428
+ restrictToPreferredProvider: false,
429
+ });
430
+ if (!resolved) return null;
431
+
432
+ const models = await listModelInfos(client, directory);
433
+ const modelInfo = findModelInfo(models, resolved.providerID, resolved.modelID);
434
+ const outputTokenLimit = Number(modelInfo?.limit?.output) > 0 ? Number(modelInfo.limit.output) : null;
435
+
287
436
  // Two passes: the first only to learn the context, which a caller-supplied
288
437
  // reserve function needs before it can answer.
289
- const { contextTokens, contextKnown } = getModelInputCharBudget({
290
- catalog,
291
- providerID: resolved.providerID,
292
- modelID: resolved.modelID,
293
- });
438
+ const { contextTokens, contextKnown } = getModelInputCharBudget({ modelInfo });
294
439
  const reserveTokens = resolveReserveTokens(outputReserveTokens, { contextTokens, outputTokenLimit });
295
- const { maxChars } = getModelInputCharBudget({
296
- catalog,
297
- providerID: resolved.providerID,
298
- modelID: resolved.modelID,
299
- outputReserveTokens: reserveTokens,
300
- });
440
+ const { maxChars } = getModelInputCharBudget({ modelInfo, outputReserveTokens: reserveTokens });
301
441
 
302
- // Settings/config/request overrides can name a provider with no usable login.
303
- // Report that here so readiness can refuse before the user pays for a 401.
304
- const hasLogin = Boolean(await resolveProviderLogin({
305
- auth,
306
- workingDirectory: directory,
307
- providerID: resolved.providerID,
308
- }));
442
+ // An override can name a model OpenCode has no credential for. It reports
443
+ // that as a disabled model, and readiness refuses before the user pays for
444
+ // a failed request. A model we cannot find at all is not evidence either
445
+ // way, so it counts as usable.
446
+ const hasLogin = modelInfo ? modelInfo.enabled !== false : true;
309
447
 
310
448
  return {
311
449
  ...resolved,
@@ -316,7 +454,7 @@ export async function describeSmallModel({ directory, preferredProviderID, prefe
316
454
  // What the caller should ask for, so the request and the reserve above
317
455
  // cannot drift apart.
318
456
  outputTokens: Number(reserveTokens) > 0 ? Number(reserveTokens) : null,
319
- structuredOutput: typeof entry?.structured_output === 'boolean' ? entry.structured_output : null,
457
+ structuredOutput: null,
320
458
  outputTokenLimit,
321
459
  };
322
460
  }