@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
@@ -45,19 +45,21 @@ describe('opencode plugins data layer', () => {
45
45
  delete process.env.OPENCODE_CONFIG;
46
46
  });
47
47
 
48
- test('parses raw plugin entries', () => {
48
+ test('parses v1 tuples and v2 objects into the same entry', () => {
49
49
  expect(plugins.parsePluginRaw('foo')).toEqual({ spec: 'foo' });
50
50
  expect(plugins.parsePluginRaw('foo@1.0.0')).toEqual({ spec: 'foo@1.0.0' });
51
51
  expect(plugins.parsePluginRaw(['foo', { a: 1 }])).toEqual({ spec: 'foo', options: { a: 1 } });
52
- expect(plugins.parsePluginRaw(['foo', {}])).toEqual({ spec: 'foo', options: {} });
52
+ expect(plugins.parsePluginRaw(['foo', {}])).toEqual({ spec: 'foo' });
53
+ expect(plugins.parsePluginRaw({ package: 'foo', options: { a: 1 } })).toEqual({ spec: 'foo', options: { a: 1 } });
54
+ expect(plugins.parsePluginRaw({ package: 'foo' })).toEqual({ spec: 'foo' });
53
55
  expect(() => plugins.parsePluginRaw(123)).toThrow('Plugin spec');
54
56
  });
55
57
 
56
- test('serializes plugin entries', () => {
58
+ test('serializes plugin entries in v2 shape, never a tuple', () => {
57
59
  expect(plugins.serializePluginEntry({ spec: 'foo' })).toBe('foo');
58
60
  expect(plugins.serializePluginEntry({ spec: 'foo', options: undefined })).toBe('foo');
59
61
  expect(plugins.serializePluginEntry({ spec: 'foo', options: {} })).toBe('foo');
60
- expect(plugins.serializePluginEntry({ spec: 'foo', options: { a: 1 } })).toEqual(['foo', { a: 1 }]);
62
+ expect(plugins.serializePluginEntry({ spec: 'foo', options: { a: 1 } })).toEqual({ package: 'foo', options: { a: 1 } });
61
63
  });
62
64
 
63
65
  test('rejects invalid specs and file names', () => {
@@ -69,11 +71,12 @@ describe('opencode plugins data layer', () => {
69
71
  expect(() => plugins.writePluginDirFile({ fileName: 'foo.txt', content: '', scope: 'project' }, projectDir)).toThrow('Plugin file name');
70
72
  });
71
73
 
72
- test('creates string and tuple entries with duplicate rejection', () => {
74
+ test('creates string and object entries with duplicate rejection', () => {
73
75
  plugins.createPluginEntry({ spec: 'plain-plugin', scope: 'user' }, projectDir);
74
76
  plugins.createPluginEntry({ spec: 'tuple-plugin', options: { apiKey: 'x' }, scope: 'user' }, projectDir);
75
77
 
76
- expect(readJson(userConfigPath).plugin).toEqual(['plain-plugin', ['tuple-plugin', { apiKey: 'x' }]]);
78
+ expect(readJson(userConfigPath).plugins).toEqual(['plain-plugin', { package: 'tuple-plugin', options: { apiKey: 'x' } }]);
79
+ expect(readJson(userConfigPath).plugin).toBeUndefined();
77
80
  expect(() => plugins.createPluginEntry({ spec: 'plain-plugin', scope: 'user' }, projectDir)).toThrow('already exists');
78
81
  expect(thrownBy(() => plugins.createPluginEntry({ spec: 'plain-plugin', scope: 'user' }, projectDir))).toHaveProperty('code', 'ENTRY_EXISTS');
79
82
  });
@@ -82,8 +85,8 @@ describe('opencode plugins data layer', () => {
82
85
  plugins.createPluginEntry({ spec: 'user-plugin', scope: 'user' }, projectDir);
83
86
  plugins.createPluginEntry({ spec: 'project-plugin', scope: 'project' }, projectDir);
84
87
 
85
- expect(readJson(userConfigPath).plugin).toEqual(['user-plugin']);
86
- expect(readJson(path.join(projectDir, '.opencode', 'opencode.json')).plugin).toEqual(['project-plugin']);
88
+ expect(readJson(userConfigPath).plugins).toEqual(['user-plugin']);
89
+ expect(readJson(path.join(projectDir, '.opencode', 'opencode.json')).plugins).toEqual(['project-plugin']);
87
90
  });
88
91
 
89
92
  test('re-resolves custom config env between calls', () => {
@@ -98,24 +101,34 @@ describe('opencode plugins data layer', () => {
98
101
  plugins.createPluginEntry({ spec: 'second-plugin', scope: 'user' }, projectDir);
99
102
  plugins.writePluginDirFile({ fileName: 'second.js', content: 'two', scope: 'user' }, projectDir);
100
103
 
101
- expect(readJson(firstConfigPath).plugin).toEqual(['first-plugin']);
102
- expect(readJson(secondConfigPath).plugin).toEqual(['second-plugin']);
104
+ expect(readJson(firstConfigPath).plugins).toEqual(['first-plugin']);
105
+ expect(readJson(secondConfigPath).plugins).toEqual(['second-plugin']);
103
106
  expect(fs.existsSync(path.join(path.dirname(firstConfigPath), 'plugins', 'first.js'))).toBe(true);
104
107
  expect(fs.existsSync(path.join(path.dirname(secondConfigPath), 'plugins', 'second.js'))).toBe(true);
105
108
  });
106
109
 
107
- test('updates entries in place and transitions between string and tuple', () => {
110
+ test('rewrites a v1 plugin array into v2 plugins when one entry is updated', () => {
108
111
  writeJson(userConfigPath, { plugin: ['first', ['second', { a: 1 }], 'third'] });
109
112
 
110
113
  plugins.updatePluginEntry(plugins.encodePluginId('config', 'user:second'), { spec: 'second-new', options: {} }, projectDir);
111
- expect(readJson(userConfigPath).plugin).toEqual(['first', 'second-new', 'third']);
114
+ const migrated = readJson(userConfigPath);
115
+ expect(migrated.plugin).toBeUndefined();
116
+ expect(migrated.plugins).toEqual(['first', 'second-new', 'third']);
112
117
 
113
118
  plugins.updatePluginEntry(plugins.encodePluginId('config', 'user:first'), { spec: 'first-new', options: { b: 2 } }, projectDir);
114
- expect(readJson(userConfigPath).plugin).toEqual([['first-new', { b: 2 }], 'second-new', 'third']);
119
+ expect(readJson(userConfigPath).plugins).toEqual([{ package: 'first-new', options: { b: 2 } }, 'second-new', 'third']);
115
120
  });
116
121
 
117
- test('deletes entries and prunes empty plugin key', () => {
118
- writeJson(userConfigPath, { plugin: ['only'] });
122
+ test('reads a v2 plugins array and updates it in place', () => {
123
+ writeJson(userConfigPath, { plugins: ['alpha', { package: 'beta', options: { a: 1 } }] });
124
+
125
+ expect(plugins.listPluginEntries(projectDir).map((entry) => entry.spec)).toEqual(['alpha', 'beta']);
126
+ plugins.updatePluginEntry(plugins.encodePluginId('config', 'user:beta'), { options: { a: 2 } }, projectDir);
127
+ expect(readJson(userConfigPath).plugins).toEqual(['alpha', { package: 'beta', options: { a: 2 } }]);
128
+ });
129
+
130
+ test('deletes entries and prunes the empty plugins key', () => {
131
+ writeJson(userConfigPath, { plugins: ['only'] });
119
132
 
120
133
  plugins.deletePluginEntry(plugins.encodePluginId('config', 'user:only'), projectDir);
121
134
  expect(readJson(userConfigPath)).toEqual({});
@@ -5,6 +5,15 @@ import {
5
5
  getConfigForPath,
6
6
  writeConfig,
7
7
  } from './shared.js';
8
+ import {
9
+ readSectionEntry,
10
+ writeSectionEntry,
11
+ deleteSectionEntry,
12
+ toModelVariants,
13
+ toProviderEntity,
14
+ toProviderPackage,
15
+ toNpmPackage,
16
+ } from './config-v2.js';
8
17
 
9
18
  const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-_]*$/;
10
19
  const BASE_URL_PATTERN = /^https?:\/\//;
@@ -15,34 +24,25 @@ const CUSTOM_PROVIDER_NPM_PACKAGES = new Set([
15
24
  '@ai-sdk/anthropic',
16
25
  ]);
17
26
 
27
+ // OpenCode 2 keeps providers under `providers` with `package: "aisdk:<npm>"`,
28
+ // `settings.baseURL`, and `models.<id>.modelID`. The v1 `provider` map with
29
+ // `npm`/`api`/`options` is still decoded, so reads accept it; every write is v2.
30
+
31
+ function providerExistsIn(config, providerId) {
32
+ return readSectionEntry(config, 'providers', providerId).value !== undefined;
33
+ }
34
+
18
35
  function getProviderSources(providerId, workingDirectory) {
19
36
  const layers = readConfigLayers(workingDirectory);
20
37
  const { userConfig, projectConfig, customConfig, paths } = layers;
21
38
 
22
- const customProviders = isPlainObject(customConfig?.provider) ? customConfig.provider : {};
23
- const customProvidersAlias = isPlainObject(customConfig?.providers) ? customConfig.providers : {};
24
- const projectProviders = isPlainObject(projectConfig?.provider) ? projectConfig.provider : {};
25
- const projectProvidersAlias = isPlainObject(projectConfig?.providers) ? projectConfig.providers : {};
26
- const userProviders = isPlainObject(userConfig?.provider) ? userConfig.provider : {};
27
- const userProvidersAlias = isPlainObject(userConfig?.providers) ? userConfig.providers : {};
28
-
29
- const customExists =
30
- Object.prototype.hasOwnProperty.call(customProviders, providerId) ||
31
- Object.prototype.hasOwnProperty.call(customProvidersAlias, providerId);
32
- const projectExists =
33
- Object.prototype.hasOwnProperty.call(projectProviders, providerId) ||
34
- Object.prototype.hasOwnProperty.call(projectProvidersAlias, providerId);
35
- const userExists =
36
- Object.prototype.hasOwnProperty.call(userProviders, providerId) ||
37
- Object.prototype.hasOwnProperty.call(userProvidersAlias, providerId);
38
-
39
39
  return {
40
40
  sources: {
41
41
  auth: { exists: false },
42
- user: { exists: userExists, path: paths.userPath },
43
- project: { exists: projectExists, path: paths.projectPath || null },
44
- custom: { exists: customExists, path: paths.customPath }
45
- }
42
+ user: { exists: providerExistsIn(userConfig, providerId), path: paths.userPath },
43
+ project: { exists: providerExistsIn(projectConfig, providerId), path: paths.projectPath || null },
44
+ custom: { exists: providerExistsIn(customConfig, providerId), path: paths.customPath },
45
+ },
46
46
  };
47
47
  }
48
48
 
@@ -50,6 +50,9 @@ function getProviderSources(providerId, workingDirectory) {
50
50
  * Validate a custom provider config payload before persistence.
51
51
  * Returns { ok: true, value } or { ok: false, error }.
52
52
  *
53
+ * Accepts the v2 spelling (`package`, `settings.baseURL`) and the v1 spelling
54
+ * (`npm`, `options.baseURL`); the normalized value is always v2.
55
+ *
53
56
  * Credentials: either config.env contains a variable name, or hasStoredAuth is true
54
57
  * (auth.json already has a key — typically after auth.set, or when editing).
55
58
  */
@@ -67,17 +70,19 @@ function validateCustomProviderConfig(providerId, config, options = {}) {
67
70
  return { ok: false, error: 'Provider name is required' };
68
71
  }
69
72
 
70
- const npm = typeof config.npm === 'string' ? config.npm.trim() : OPENAI_COMPATIBLE_NPM;
73
+ const npm = toNpmPackage(config.package ?? config.npm) || OPENAI_COMPATIBLE_NPM;
71
74
  if (!CUSTOM_PROVIDER_NPM_PACKAGES.has(npm)) {
72
75
  return { ok: false, error: 'Custom providers must use @ai-sdk/openai-compatible, @ai-sdk/openai, or @ai-sdk/anthropic' };
73
76
  }
74
77
 
75
- const optionsBlock = isPlainObject(config.options) ? config.options : null;
76
- if (!optionsBlock) {
77
- return { ok: false, error: 'Provider options are required' };
78
+ const settingsBlock = isPlainObject(config.settings)
79
+ ? config.settings
80
+ : (isPlainObject(config.options) ? config.options : null);
81
+ if (!settingsBlock) {
82
+ return { ok: false, error: 'Provider settings are required' };
78
83
  }
79
84
 
80
- const baseURL = typeof optionsBlock.baseURL === 'string' ? optionsBlock.baseURL.trim() : '';
85
+ const baseURL = typeof settingsBlock.baseURL === 'string' ? settingsBlock.baseURL.trim() : '';
81
86
  if (!baseURL) {
82
87
  return { ok: false, error: 'Base URL is required' };
83
88
  }
@@ -103,15 +108,17 @@ function validateCustomProviderConfig(providerId, config, options = {}) {
103
108
  if (!modelName) {
104
109
  return { ok: false, error: `Model "${trimmedId}" requires a name` };
105
110
  }
106
- normalizedModels[trimmedId] = { name: modelName };
111
+ normalizedModels[trimmedId] = { modelID: trimmedId, name: modelName };
112
+ // Present means the caller owns the levels; an empty list removes them.
113
+ if (Array.isArray(modelValue.variants)) {
114
+ normalizedModels[trimmedId].variants = toModelVariants(modelValue.variants);
115
+ }
107
116
  }
108
117
 
109
118
  const normalized = {
110
- npm,
119
+ package: toProviderPackage(npm),
111
120
  name,
112
- options: {
113
- baseURL,
114
- },
121
+ settings: { baseURL },
115
122
  models: normalizedModels,
116
123
  };
117
124
 
@@ -133,9 +140,10 @@ function validateCustomProviderConfig(providerId, config, options = {}) {
133
140
  };
134
141
  }
135
142
 
136
- if (isPlainObject(optionsBlock.headers)) {
143
+ const headerSource = isPlainObject(config.headers) ? config.headers : settingsBlock.headers;
144
+ if (isPlainObject(headerSource)) {
137
145
  const headers = {};
138
- for (const [headerKey, headerValue] of Object.entries(optionsBlock.headers)) {
146
+ for (const [headerKey, headerValue] of Object.entries(headerSource)) {
139
147
  if (typeof headerKey !== 'string' || !headerKey.trim()) {
140
148
  continue;
141
149
  }
@@ -145,7 +153,7 @@ function validateCustomProviderConfig(providerId, config, options = {}) {
145
153
  headers[headerKey.trim()] = headerValue.trim();
146
154
  }
147
155
  if (Object.keys(headers).length > 0) {
148
- normalized.options.headers = headers;
156
+ normalized.headers = headers;
149
157
  }
150
158
  }
151
159
 
@@ -153,34 +161,38 @@ function validateCustomProviderConfig(providerId, config, options = {}) {
153
161
  }
154
162
 
155
163
  function mergeCustomProviderConfig(existingValue, normalizedConfig) {
156
- const existing = isPlainObject(existingValue) ? existingValue : {};
157
- const existingOptions = isPlainObject(existing.options) ? existing.options : {};
158
- const normalizedOptions = isPlainObject(normalizedConfig.options) ? normalizedConfig.options : {};
159
- const mergedOptions = { ...existingOptions, ...normalizedOptions };
160
- if (!Object.prototype.hasOwnProperty.call(normalizedOptions, 'headers')) {
161
- delete mergedOptions.headers;
162
- }
164
+ // Read the existing entry through the v2 projection so a legacy
165
+ // `npm`/`api`/`options` block is carried forward in native shape.
166
+ const existing = toProviderEntity(existingValue);
167
+ const mergedSettings = { ...(existing.settings ?? {}), ...(normalizedConfig.settings ?? {}) };
163
168
 
164
169
  const existingModels = isPlainObject(existing.models) ? existing.models : {};
165
170
  const normalizedModels = isPlainObject(normalizedConfig.models) ? normalizedConfig.models : {};
166
171
  const mergedModels = Object.fromEntries(
167
172
  Object.entries(normalizedModels).map(([modelId, normalizedModel]) => {
168
173
  const existingModel = isPlainObject(existingModels[modelId]) ? existingModels[modelId] : {};
169
- const nextModel = isPlainObject(normalizedModel) ? normalizedModel : {};
170
- return [modelId, { ...existingModel, ...nextModel }];
174
+ const mergedModel = { ...existingModel, ...normalizedModel };
175
+ if (Array.isArray(mergedModel.variants) && mergedModel.variants.length === 0) {
176
+ delete mergedModel.variants;
177
+ }
178
+ return [modelId, mergedModel];
171
179
  }),
172
180
  );
173
181
 
174
182
  const merged = {
175
183
  ...existing,
176
184
  ...normalizedConfig,
177
- options: mergedOptions,
185
+ settings: mergedSettings,
178
186
  models: mergedModels,
179
187
  };
188
+ // Headers and env are explicit removals when the form omits them.
189
+ if (!Object.prototype.hasOwnProperty.call(normalizedConfig, 'headers')) {
190
+ delete merged.headers;
191
+ }
180
192
  if (!Object.prototype.hasOwnProperty.call(normalizedConfig, 'env')) {
181
193
  delete merged.env;
182
194
  }
183
- return merged;
195
+ return toProviderEntity(merged);
184
196
  }
185
197
 
186
198
  /**
@@ -213,20 +225,11 @@ function upsertProviderConfig(providerId, config, workingDirectory, scope = 'use
213
225
  }
214
226
 
215
227
  const targetConfig = getConfigForPath(layers, targetPath);
216
- const providerConfig = isPlainObject(targetConfig.provider) ? { ...targetConfig.provider } : {};
217
- const providersAlias = isPlainObject(targetConfig.providers) ? { ...targetConfig.providers } : {};
218
- const existingProvider = providerConfig[validated.value.providerId] ?? providersAlias[validated.value.providerId];
219
- const mergedConfig = mergeCustomProviderConfig(existingProvider, validated.value.config);
220
- providerConfig[validated.value.providerId] = mergedConfig;
221
- targetConfig.provider = providerConfig;
222
- if (Object.prototype.hasOwnProperty.call(providersAlias, validated.value.providerId)) {
223
- delete providersAlias[validated.value.providerId];
224
- if (Object.keys(providersAlias).length === 0) {
225
- delete targetConfig.providers;
226
- } else {
227
- targetConfig.providers = providersAlias;
228
- }
229
- }
228
+ const existing = readSectionEntry(targetConfig, 'providers', validated.value.providerId).value;
229
+ const mergedConfig = mergeCustomProviderConfig(existing, validated.value.config);
230
+ // Writes `providers`; a legacy `provider.<id>` in the same file is dropped so
231
+ // the two spellings cannot disagree.
232
+ writeSectionEntry(targetConfig, 'providers', validated.value.providerId, mergedConfig);
230
233
 
231
234
  if (Array.isArray(targetConfig.disabled_providers)) {
232
235
  targetConfig.disabled_providers = targetConfig.disabled_providers.filter(
@@ -265,33 +268,10 @@ function removeProviderConfig(providerId, workingDirectory, scope = 'user') {
265
268
  }
266
269
 
267
270
  const targetConfig = getConfigForPath(layers, targetPath);
268
- const providerConfig = isPlainObject(targetConfig.provider) ? targetConfig.provider : {};
269
- const providersConfig = isPlainObject(targetConfig.providers) ? targetConfig.providers : {};
270
- const removedProvider = Object.prototype.hasOwnProperty.call(providerConfig, providerId);
271
- const removedProviders = Object.prototype.hasOwnProperty.call(providersConfig, providerId);
272
-
273
- if (!removedProvider && !removedProviders) {
271
+ if (!deleteSectionEntry(targetConfig, 'providers', providerId)) {
274
272
  return false;
275
273
  }
276
274
 
277
- if (removedProvider) {
278
- delete providerConfig[providerId];
279
- if (Object.keys(providerConfig).length === 0) {
280
- delete targetConfig.provider;
281
- } else {
282
- targetConfig.provider = providerConfig;
283
- }
284
- }
285
-
286
- if (removedProviders) {
287
- delete providersConfig[providerId];
288
- if (Object.keys(providersConfig).length === 0) {
289
- delete targetConfig.providers;
290
- } else {
291
- targetConfig.providers = providersConfig;
292
- }
293
- }
294
-
295
275
  writeConfig(targetConfig, targetPath || CONFIG_FILE);
296
276
  console.log(`Removed provider ${providerId} from config: ${targetPath}`);
297
277
  return true;
@@ -82,7 +82,7 @@ describe('custom provider config persistence', () => {
82
82
  models: { m: { name: 'M' } },
83
83
  });
84
84
  expect(result.ok).toBe(true);
85
- expect(result.value.config.npm).toBe(npm);
85
+ expect(result.value.config.package).toBe(`aisdk:${npm}`);
86
86
  }
87
87
  });
88
88
 
@@ -118,16 +118,15 @@ describe('custom provider config persistence', () => {
118
118
  expect(result.path.startsWith(projectDir)).toBe(true);
119
119
 
120
120
  const written = readJson(result.path);
121
- expect(written.provider['campus-llm']).toEqual({
122
- npm: '@ai-sdk/openai-compatible',
121
+ expect(written.provider).toBeUndefined();
122
+ expect(written.providers['campus-llm']).toEqual({
123
123
  name: 'Campus LLM',
124
+ package: 'aisdk:@ai-sdk/openai-compatible',
124
125
  env: ['CAMPUS_KEY'],
125
- options: {
126
- baseURL: 'https://llm.example.edu/v1',
127
- headers: { 'X-Campus': '1' },
128
- },
126
+ settings: { baseURL: 'https://llm.example.edu/v1' },
127
+ headers: { 'X-Campus': '1' },
129
128
  models: {
130
- 'fast-model': { name: 'Fast' },
129
+ 'fast-model': { modelID: 'fast-model', name: 'Fast' },
131
130
  },
132
131
  });
133
132
 
@@ -136,8 +135,32 @@ describe('custom provider config persistence', () => {
136
135
  expect(sources.sources.project.path).toBe(result.path);
137
136
  });
138
137
 
138
+ test('upsertProviderConfig writes, keeps and clears model variants', () => {
139
+ const base = {
140
+ name: 'Campus LLM',
141
+ npm: '@ai-sdk/openai-compatible',
142
+ options: { baseURL: 'https://llm.example.edu/v1' },
143
+ env: ['CAMPUS_KEY'],
144
+ };
145
+ const result = upsertProviderConfig('campus-llm', {
146
+ ...base,
147
+ models: { m: { name: 'M', variants: [{ id: 'low', settings: { reasoningEffort: 'low' } }] } },
148
+ }, projectDir, 'project');
149
+ expect(readJson(result.path).providers['campus-llm'].models.m.variants).toEqual([
150
+ { id: 'low', settings: { reasoningEffort: 'low' } },
151
+ ]);
152
+
153
+ upsertProviderConfig('campus-llm', { ...base, models: { m: { name: 'M' } } }, projectDir, 'project');
154
+ expect(readJson(result.path).providers['campus-llm'].models.m.variants).toEqual([
155
+ { id: 'low', settings: { reasoningEffort: 'low' } },
156
+ ]);
157
+
158
+ upsertProviderConfig('campus-llm', { ...base, models: { m: { name: 'M', variants: [] } } }, projectDir, 'project');
159
+ expect(readJson(result.path).providers['campus-llm'].models.m).toEqual({ modelID: 'm', name: 'M' });
160
+ });
161
+
139
162
  test('upsertProviderConfig updates existing entry and clears disabled_providers', () => {
140
- const configPath = path.join(projectDir, 'opencode.json');
163
+ const configPath = path.join(projectDir, '.opencode', 'opencode.json');
141
164
  writeJson(configPath, {
142
165
  provider: {
143
166
  'campus-llm': {
@@ -158,13 +181,14 @@ describe('custom provider config persistence', () => {
158
181
  }, projectDir, 'project');
159
182
 
160
183
  const written = readJson(configPath);
161
- expect(written.provider['campus-llm'].name).toBe('Campus LLM');
162
- expect(written.provider['campus-llm'].models).toEqual({ b: { name: 'B' } });
184
+ expect(written.provider).toBeUndefined();
185
+ expect(written.providers['campus-llm'].name).toBe('Campus LLM');
186
+ expect(written.providers['campus-llm'].models).toEqual({ b: { modelID: 'b', name: 'B' } });
163
187
  expect(written.disabled_providers).toEqual(['other']);
164
188
  });
165
189
 
166
190
  test('upsertProviderConfig preserves unmanaged provider and model metadata', () => {
167
- const configPath = path.join(projectDir, 'opencode.json');
191
+ const configPath = path.join(projectDir, '.opencode', 'opencode.json');
168
192
  writeJson(configPath, {
169
193
  provider: {
170
194
  'campus-llm': {
@@ -210,69 +234,116 @@ describe('custom provider config persistence', () => {
210
234
  },
211
235
  }, projectDir, 'project', { hasStoredAuth: true });
212
236
 
213
- const written = readJson(configPath).provider['campus-llm'];
214
- expect(written).toEqual({
215
- npm: '@ai-sdk/openai-compatible',
237
+ // A legacy `provider` entry is rewritten in place into `providers` in v2
238
+ // shape: `npm` -> `package`, `options` -> `settings`, `tool_call`/
239
+ // `modalities` -> `capabilities`, variants map -> variants array. Fields v2
240
+ // accepts but ignores (`reasoning`, `attachment`) are dropped.
241
+ const config = readJson(configPath);
242
+ expect(config.provider).toBeUndefined();
243
+ expect(config.providers['campus-llm']).toEqual({
216
244
  name: 'Campus LLM',
217
- customProviderField: { owner: 'user' },
218
- options: {
245
+ package: 'aisdk:@ai-sdk/openai-compatible',
246
+ settings: {
219
247
  baseURL: 'https://new.example.edu/v1',
220
248
  timeout: 45_000,
221
249
  },
222
250
  models: {
223
251
  retained: {
252
+ modelID: 'retained',
224
253
  name: 'Retained model',
225
- reasoning: true,
226
- attachment: true,
227
- tool_call: true,
228
- modalities: { input: ['text', 'image', 'pdf'], output: ['text'] },
254
+ settings: { instructions: 'Keep this instruction' },
255
+ capabilities: { tools: true, input: ['text', 'image', 'pdf'], output: ['text'] },
256
+ variants: [
257
+ { id: 'low', settings: { reasoningEffort: 'low' } },
258
+ { id: 'high', settings: { reasoningEffort: 'high' } },
259
+ ],
229
260
  limit: { context: 1_050_000, input: 922_000, output: 128_000 },
230
- options: { instructions: 'Keep this instruction' },
231
- variants: {
232
- low: { reasoningEffort: 'low' },
233
- high: { reasoningEffort: 'high' },
261
+ },
262
+ added: { modelID: 'added', name: 'Added model' },
263
+ },
264
+ });
265
+ });
266
+
267
+ test('renaming a provider keeps canonical and model compatibility', () => {
268
+ const configPath = path.join(projectDir, '.opencode', 'opencode.json');
269
+ writeJson(configPath, {
270
+ providers: {
271
+ 'openai-proxy': {
272
+ canonical: 'openai',
273
+ name: 'Old name',
274
+ package: 'aisdk:@ai-sdk/openai-compatible',
275
+ env: ['PROXY_KEY'],
276
+ settings: { baseURL: 'https://proxy.example.com/v1' },
277
+ models: {
278
+ 'gpt-5': {
279
+ modelID: 'gpt-5',
280
+ name: 'GPT-5',
281
+ compatibility: { reasoningField: 'reasoning_content', requireReasoning: true, maxTokensField: 'max_tokens' },
282
+ },
234
283
  },
235
- customModelField: { source: 'manual' },
236
284
  },
237
- added: { name: 'Added model' },
285
+ },
286
+ });
287
+
288
+ upsertProviderConfig('openai-proxy', {
289
+ name: 'New name',
290
+ env: ['PROXY_KEY'],
291
+ settings: { baseURL: 'https://proxy.example.com/v1' },
292
+ models: { 'gpt-5': { name: 'GPT-5' } },
293
+ }, projectDir, 'project');
294
+
295
+ expect(readJson(configPath).providers['openai-proxy']).toEqual({
296
+ canonical: 'openai',
297
+ name: 'New name',
298
+ package: 'aisdk:@ai-sdk/openai-compatible',
299
+ env: ['PROXY_KEY'],
300
+ settings: { baseURL: 'https://proxy.example.com/v1' },
301
+ models: {
302
+ 'gpt-5': {
303
+ modelID: 'gpt-5',
304
+ name: 'GPT-5',
305
+ compatibility: { reasoningField: 'reasoning_content', requireReasoning: true, maxTokensField: 'max_tokens' },
306
+ },
238
307
  },
239
308
  });
240
309
  });
241
310
 
242
- test('upsertProviderConfig preserves metadata while migrating the legacy providers alias', () => {
243
- const configPath = path.join(projectDir, 'opencode.json');
311
+ test('accepts a native v2 payload and keeps the entry in providers', () => {
312
+ const configPath = path.join(projectDir, '.opencode', 'opencode.json');
244
313
  writeJson(configPath, {
245
314
  providers: {
246
- legacy: {
247
- name: 'Legacy provider',
248
- options: { baseURL: 'https://old.example.com/v1', timeout: 30_000 },
249
- models: { model: { name: 'Old model', reasoning: true } },
315
+ native: {
316
+ name: 'Native provider',
317
+ package: 'aisdk:@ai-sdk/openai-compatible',
318
+ settings: { baseURL: 'https://old.example.com/v1', timeout: 30_000 },
319
+ models: { model: { modelID: 'model', name: 'Old model' } },
250
320
  },
251
321
  },
252
322
  });
253
323
 
254
- upsertProviderConfig('legacy', {
324
+ upsertProviderConfig('native', {
255
325
  name: 'Updated provider',
256
- options: { baseURL: 'https://new.example.com/v1' },
326
+ package: 'aisdk:@ai-sdk/openai-compatible',
327
+ settings: { baseURL: 'https://new.example.com/v1' },
257
328
  models: { model: { name: 'Updated model' } },
258
329
  }, projectDir, 'project', { hasStoredAuth: true });
259
330
 
260
331
  const written = readJson(configPath);
261
- expect(written.providers).toBeUndefined();
262
- expect(written.provider.legacy).toEqual({
263
- npm: '@ai-sdk/openai-compatible',
332
+ expect(written.provider).toBeUndefined();
333
+ expect(written.providers.native).toEqual({
264
334
  name: 'Updated provider',
265
- options: { baseURL: 'https://new.example.com/v1', timeout: 30_000 },
266
- models: { model: { name: 'Updated model', reasoning: true } },
335
+ package: 'aisdk:@ai-sdk/openai-compatible',
336
+ settings: { baseURL: 'https://new.example.com/v1', timeout: 30_000 },
337
+ models: { model: { modelID: 'model', name: 'Updated model' } },
267
338
  });
268
339
  });
269
340
 
270
- test('migrating one legacy providers entry keeps the other legacy entries', () => {
271
- const configPath = path.join(projectDir, 'opencode.json');
341
+ test('rewriting one legacy provider entry leaves the other legacy entries alone', () => {
342
+ const configPath = path.join(projectDir, '.opencode', 'opencode.json');
272
343
  writeJson(configPath, {
273
- providers: {
274
- legacy: { name: 'Legacy provider', options: { baseURL: 'https://old.example.com/v1' }, models: { model: { name: 'Old model' } } },
275
- untouched: { name: 'Untouched', options: { baseURL: 'https://other.example.com/v1' }, models: { model: { name: 'Other model' } } },
344
+ provider: {
345
+ legacy: { npm: '@ai-sdk/openai-compatible', name: 'Legacy provider', options: { baseURL: 'https://old.example.com/v1' }, models: { model: { name: 'Old model' } } },
346
+ untouched: { npm: '@ai-sdk/openai-compatible', name: 'Untouched', options: { baseURL: 'https://other.example.com/v1' }, models: { model: { name: 'Other model' } } },
276
347
  },
277
348
  });
278
349
 
@@ -283,10 +354,10 @@ describe('custom provider config persistence', () => {
283
354
  }, projectDir, 'project', { hasStoredAuth: true });
284
355
 
285
356
  const written = readJson(configPath);
286
- expect(written.providers).toEqual({
287
- untouched: { name: 'Untouched', options: { baseURL: 'https://other.example.com/v1' }, models: { model: { name: 'Other model' } } },
357
+ expect(written.provider).toEqual({
358
+ untouched: { npm: '@ai-sdk/openai-compatible', name: 'Untouched', options: { baseURL: 'https://other.example.com/v1' }, models: { model: { name: 'Other model' } } },
288
359
  });
289
- expect(written.provider.legacy.name).toBe('Updated provider');
360
+ expect(written.providers.legacy.name).toBe('Updated provider');
290
361
  });
291
362
 
292
363
  test('upsert then remove restores absence', () => {
@@ -303,7 +374,7 @@ describe('custom provider config persistence', () => {
303
374
  });
304
375
 
305
376
  test('failed validation does not write config', () => {
306
- const configPath = path.join(projectDir, 'opencode.json');
377
+ const configPath = path.join(projectDir, '.opencode', 'opencode.json');
307
378
  expect(() => upsertProviderConfig('ok', {
308
379
  name: 'X',
309
380
  options: { baseURL: 'not-a-url' },
@@ -326,7 +397,7 @@ describe('custom provider config persistence', () => {
326
397
 
327
398
  test('project-scope edit updates project layer without creating a user entry', () => {
328
399
  const providerId = `proj-scope-${Date.now()}`;
329
- const configPath = path.join(projectDir, 'opencode.json');
400
+ const configPath = path.join(projectDir, '.opencode', 'opencode.json');
330
401
 
331
402
  upsertProviderConfig(providerId, {
332
403
  name: 'Project Scoped',
@@ -341,14 +412,12 @@ describe('custom provider config persistence', () => {
341
412
  }, projectDir, 'project', { hasStoredAuth: true });
342
413
 
343
414
  const written = readJson(configPath);
344
- expect(written.provider[providerId]).toEqual({
345
- npm: '@ai-sdk/openai-compatible',
415
+ expect(written.providers[providerId]).toEqual({
346
416
  name: 'Project Scoped Updated',
347
- options: {
348
- baseURL: 'https://project.example.com/v2',
349
- headers: { 'X-Project': '1' },
350
- },
351
- models: { m: { name: 'M2' } },
417
+ package: 'aisdk:@ai-sdk/openai-compatible',
418
+ settings: { baseURL: 'https://project.example.com/v2' },
419
+ headers: { 'X-Project': '1' },
420
+ models: { m: { modelID: 'm', name: 'M2' } },
352
421
  });
353
422
 
354
423
  const sources = getProviderSources(providerId, projectDir);
@@ -387,8 +456,8 @@ describe('custom provider config persistence', () => {
387
456
  }, projectDir, 'custom', { hasStoredAuth: true });
388
457
 
389
458
  const written = readJson(customPath);
390
- expect(written.provider[providerId].name).toBe('Custom Scoped Updated');
391
- expect(written.provider[providerId].options.baseURL).toBe('https://custom.example.com/v2');
459
+ expect(written.providers[providerId].name).toBe('Custom Scoped Updated');
460
+ expect(written.providers[providerId].settings.baseURL).toBe('https://custom.example.com/v2');
392
461
 
393
462
  const sources = getProviderSources(providerId, projectDir);
394
463
  expect(sources.sources.custom.exists).toBe(true);