@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.4

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 (287) hide show
  1. package/README.md +15 -11
  2. package/bin/cli-opencode.test.js +4 -4
  3. package/bin/cli.js +1 -1
  4. package/bin/lib/cli-startup.js +1 -1
  5. package/bin/lib/cli-startup.test.js +18 -4
  6. package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
  7. package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
  8. package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
  10. package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
  11. package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
  12. package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
  14. package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
  15. package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
  16. package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
  17. package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
  20. package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
  21. package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
  22. package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
  24. package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
  25. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  26. package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
  27. package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
  28. package/dist/assets/context-menu-B2LXJgXL.js +1 -0
  29. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
  30. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
  31. package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
  32. package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
  33. package/dist/assets/es-CN3E_zXj.js +15 -0
  34. package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
  35. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
  36. package/dist/assets/fr-9zZvQZD2.js +13 -0
  37. package/dist/assets/gitApi-yuWg9BN4.js +10 -0
  38. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
  39. package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
  40. package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
  41. package/dist/assets/ja-Dnjk_82N.js +15 -0
  42. package/dist/assets/ko-BxCEE99C.js +15 -0
  43. package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
  44. package/dist/assets/main-DiL6w6KF.js +169 -0
  45. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  46. package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
  47. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  48. package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
  49. package/dist/assets/miniChat-CwtUaElU.js +2 -0
  50. package/dist/assets/mobile-DB-6--ws.js +2 -0
  51. package/dist/assets/multirun-TAsxFI8e.js +4 -0
  52. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
  53. package/dist/assets/pl-CNNsWnPM.js +15 -0
  54. package/dist/assets/platform-C13fMVEs.js +6 -0
  55. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  56. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  57. package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
  58. package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
  59. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  60. package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
  61. package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
  62. package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
  63. package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
  64. package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
  65. package/dist/assets/src-ubCWXZhR.css +2 -0
  66. package/dist/assets/store-DLCiZaA6.js +15 -0
  67. package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
  68. package/dist/assets/textarea-BBOlRI9O.js +1 -0
  69. package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
  70. package/dist/assets/uk-rwQrxeYM.js +15 -0
  71. package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
  72. package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
  73. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  74. package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
  75. package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
  76. package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
  77. package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
  78. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  79. package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +2 -0
  80. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  81. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  82. package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.js} +2 -2
  83. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  84. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  85. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  86. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  87. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  88. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  89. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  90. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  91. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  92. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  93. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  94. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  95. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  96. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  97. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  98. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  99. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  100. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  101. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  102. package/dist/index.html +13 -13
  103. package/dist/mini-chat.html +13 -13
  104. package/dist/mobile.html +13 -13
  105. package/package.json +1 -1
  106. package/server/index.js +327 -62
  107. package/server/lib/assistants/DOCUMENTATION.md +14 -9
  108. package/server/lib/assistants/contracts.js +11 -1
  109. package/server/lib/assistants/projection-history.test.js +265 -18
  110. package/server/lib/assistants/routes.js +6 -0
  111. package/server/lib/assistants/service.js +417 -231
  112. package/server/lib/assistants/service.test.js +392 -148
  113. package/server/lib/conversations/DOCUMENTATION.md +1 -1
  114. package/server/lib/conversations/service.js +1 -0
  115. package/server/lib/conversations/service.test.js +16 -0
  116. package/server/lib/conversations/validation.js +11 -1
  117. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +49 -4
  120. package/server/lib/event-stream/reasoning-projection.js +16 -1
  121. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  122. package/server/lib/event-stream/runtime.js +57 -20
  123. package/server/lib/event-stream/runtime.test.js +112 -0
  124. package/server/lib/llm/DOCUMENTATION.md +33 -9
  125. package/server/lib/llm/catalog.js +1 -1
  126. package/server/lib/llm/completions.js +5 -1
  127. package/server/lib/llm/completions.test.js +3 -3
  128. package/server/lib/llm/generate.js +237 -145
  129. package/server/lib/llm/generate.test.js +201 -46
  130. package/server/lib/llm/routes.js +2 -0
  131. package/server/lib/message-queue/runtime.js +15 -2
  132. package/server/lib/notifications/runtime.js +23 -5
  133. package/server/lib/notifications/runtime.test.js +11 -0
  134. package/server/lib/opencode/DOCUMENTATION.md +85 -17
  135. package/server/lib/opencode/agent-document.d.ts +28 -0
  136. package/server/lib/opencode/agent-document.js +280 -0
  137. package/server/lib/opencode/agent-document.test.js +137 -0
  138. package/server/lib/opencode/agents.js +96 -0
  139. package/server/lib/opencode/auth-state-runtime.js +5 -0
  140. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  141. package/server/lib/opencode/config-entity-routes.js +42 -99
  142. package/server/lib/opencode/config-entity-routes.test.js +107 -0
  143. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  144. package/server/lib/opencode/ensure-cli.js +1 -2
  145. package/server/lib/opencode/env-runtime.js +40 -26
  146. package/server/lib/opencode/env-runtime.test.js +10 -17
  147. package/server/lib/opencode/feature-routes-runtime.js +43 -4
  148. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  149. package/server/lib/opencode/http-server-connections.js +98 -0
  150. package/server/lib/opencode/http-server-connections.test.js +321 -0
  151. package/server/lib/opencode/lifecycle.js +349 -39
  152. package/server/lib/opencode/lifecycle.test.js +322 -0
  153. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  154. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  155. package/server/lib/opencode/openchamber-routes.js +7 -1
  156. package/server/lib/opencode/opencode2-pin.js +4 -4
  157. package/server/lib/opencode/opencode2-pin.test.js +1 -0
  158. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
  159. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  160. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  161. package/server/lib/opencode/plugin-routes.js +3 -18
  162. package/server/lib/opencode/plugin-routes.test.js +9 -9
  163. package/server/lib/opencode/provider-catalog.js +58 -12
  164. package/server/lib/opencode/provider-catalog.test.js +48 -0
  165. package/server/lib/opencode/proxy.js +252 -46
  166. package/server/lib/opencode/proxy.test.js +235 -2
  167. package/server/lib/opencode/routes.js +296 -62
  168. package/server/lib/opencode/routes.test.js +278 -12
  169. package/server/lib/opencode/runtime-contract.js +401 -0
  170. package/server/lib/opencode/runtime-contract.test.js +214 -0
  171. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  172. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  173. package/server/lib/opencode/server-utils-runtime.js +33 -0
  174. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  175. package/server/lib/opencode/shared-service.js +147 -0
  176. package/server/lib/opencode/shared-service.test.js +93 -0
  177. package/server/lib/opencode/shared.js +3 -3
  178. package/server/lib/opencode/shutdown-runtime.js +15 -6
  179. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  180. package/server/lib/opencode/skill-routes.js +20 -33
  181. package/server/lib/opencode/skill-routes.test.js +35 -2
  182. package/server/lib/opencode/skills.test.js +15 -0
  183. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  184. package/server/lib/opencode/static-routes-runtime.js +3 -1
  185. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  186. package/server/lib/opencode/v2-client.js +14 -3
  187. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  188. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  189. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  190. package/server/lib/permission-auto-accept/runtime.js +14 -7
  191. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  192. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  193. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
  194. package/server/lib/scheduled-tasks/runtime.js +52 -4
  195. package/server/lib/scheduled-tasks/runtime.test.js +80 -1
  196. package/server/lib/session-goal/DOCUMENTATION.md +113 -13
  197. package/server/lib/session-goal/routes.js +9 -0
  198. package/server/lib/session-goal/runtime.js +1579 -117
  199. package/server/lib/session-goal/runtime.test.js +1453 -84
  200. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  201. package/server/lib/session-index/event-ingest.js +89 -16
  202. package/server/lib/session-index/event-ingest.test.js +73 -0
  203. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  204. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  205. package/server/lib/session-index/routes.js +63 -10
  206. package/server/lib/session-index/routes.test.js +8 -8
  207. package/server/lib/session-index/service.js +7 -1
  208. package/server/lib/session-index/sync-runtime.js +161 -34
  209. package/server/lib/session-index/sync-runtime.test.js +155 -1
  210. package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
  211. package/server/lib/session-metadata/routes.js +108 -9
  212. package/server/lib/session-metadata/routes.test.js +47 -2
  213. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  214. package/server/lib/session-metadata/session-archive.js +654 -0
  215. package/server/lib/session-metadata/session-archive.test.js +359 -0
  216. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  217. package/server/lib/session-metadata/session-metadata-store.js +212 -72
  218. package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
  219. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  220. package/server/lib/session-metadata/session-projection.js +255 -0
  221. package/server/lib/session-metadata/session-projection.test.js +64 -0
  222. package/server/lib/session-metadata/system-session.js +55 -0
  223. package/server/lib/session-metadata/system-session.test.js +55 -0
  224. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  225. package/server/lib/session-title/native-runtime.test.js +167 -0
  226. package/server/lib/session-title/runtime.js +67 -42
  227. package/server/lib/session-title/session-access.js +46 -0
  228. package/server/lib/session-title/session-access.test.js +91 -0
  229. package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
  230. package/server/lib/session-turn-pages/routes.js +17 -102
  231. package/server/lib/session-turn-pages/routes.test.js +281 -0
  232. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  233. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  234. package/server/lib/small-model/DOCUMENTATION.md +101 -147
  235. package/server/lib/small-model/catalog.js +31 -51
  236. package/server/lib/small-model/catalog.test.js +65 -10
  237. package/server/lib/small-model/custom-api.js +68 -0
  238. package/server/lib/small-model/custom-api.test.js +64 -0
  239. package/server/lib/small-model/index.js +124 -240
  240. package/server/lib/small-model/resolve.js +35 -120
  241. package/server/lib/small-model/resolve.test.js +20 -110
  242. package/server/lib/small-model/routes.js +1 -3
  243. package/server/lib/small-model/summary-settings.test.js +157 -253
  244. package/server/opencode-proxy.test.js +2 -0
  245. package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
  246. package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
  247. package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
  248. package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
  249. package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
  250. package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
  251. package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
  252. package/dist/assets/context-menu-jGtH6fR9.js +0 -1
  253. package/dist/assets/es-c52LTHHe.js +0 -15
  254. package/dist/assets/fr-PisFbUE8.js +0 -13
  255. package/dist/assets/gitApi-ButwzQ0N.js +0 -9
  256. package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
  257. package/dist/assets/ja-C8Ikun3Z.js +0 -15
  258. package/dist/assets/ko-ClFys5o2.js +0 -15
  259. package/dist/assets/main-Dn9rpsgl.js +0 -169
  260. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  261. package/dist/assets/miniChat-BpjxVqgg.js +0 -2
  262. package/dist/assets/mobile-nMJCaNq1.js +0 -2
  263. package/dist/assets/multirun-BvSG94mW.js +0 -4
  264. package/dist/assets/pl-CnaKWN5F.js +0 -15
  265. package/dist/assets/platform-CWTIp02q.js +0 -4
  266. package/dist/assets/projectMeta-DOe49dM_.js +0 -3
  267. package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
  268. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
  269. package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
  270. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  271. package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
  272. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
  273. package/dist/assets/src-BzX5UI-c.css +0 -2
  274. package/dist/assets/store-DJczmczv.js +0 -15
  275. package/dist/assets/textarea-3XR8sUfy.js +0 -1
  276. package/dist/assets/uk-CaX0iqkB.js +0 -15
  277. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
  278. package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
  279. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
  280. package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
  281. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  282. package/dist/assets/zh-CN-20MYEjha.js +0 -15
  283. package/dist/assets/zh-TW-DiN04qTw.js +0 -15
  284. package/server/lib/small-model/call.js +0 -427
  285. package/server/lib/small-model/call.test.js +0 -382
  286. package/server/lib/small-model/opencode-session.js +0 -183
  287. package/server/lib/small-model/opencode-session.test.js +0 -147
@@ -1,41 +1,14 @@
1
1
  import { OpenCode } from '@opencode/client';
2
2
 
3
3
  // Directory-scoped OpenCode provider catalog for small-model resolution.
4
- // Never contacts models.dev — source of truth is client.config.providers().
5
- // On OpenCode failure the loader returns an explicit minimal fallback catalog
6
- // (not an authoritative empty map) so hardcoded candidates still resolve.
4
+ // Never contacts models.dev — source of truth is provider.list + model.list,
5
+ // the same connected catalog the LLM gateway checks before generating.
6
+ // ModelInfo.id is the external model id; ModelInfo.modelID is internal.
7
+ // OpenCode failure is an explicit error, never a substitute catalog.
7
8
 
8
9
  const CATALOG_TTL_MS = 30_000;
9
10
  const CATALOG_TIMEOUT_MS = 8_000;
10
11
 
11
- // Minimal catalog when OpenCode is unreachable. OpenAI OAuth and Copilot use
12
- // fixed utility models outside the catalog; Google/Anthropic need family
13
- // entries so the family-priority scan can still pick a small model.
14
- const MINIMAL_FALLBACK_CATALOG = Object.freeze({
15
- google: Object.freeze({
16
- id: 'google',
17
- name: 'Google',
18
- models: Object.freeze({
19
- 'gemini-2.5-flash': Object.freeze({
20
- id: 'gemini-2.5-flash',
21
- family: 'gemini-flash',
22
- release_date: '2025-06-01',
23
- }),
24
- }),
25
- }),
26
- anthropic: Object.freeze({
27
- id: 'anthropic',
28
- name: 'Anthropic',
29
- models: Object.freeze({
30
- 'claude-haiku-4-5': Object.freeze({
31
- id: 'claude-haiku-4-5',
32
- family: 'claude-haiku',
33
- release_date: '2025-10-01',
34
- }),
35
- }),
36
- }),
37
- });
38
-
39
12
  const normalizeDirectoryKey = (directory) => {
40
13
  if (typeof directory !== 'string') return '';
41
14
  const trimmed = directory.trim();
@@ -46,6 +19,18 @@ const normalizeDirectoryKey = (directory) => {
46
19
 
47
20
  const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
48
21
 
22
+ // v2 ModelInfo.cost is a tier array. Ranking still reads cost.input / cost.output.
23
+ const projectCatalogCost = (cost) => {
24
+ const entry = Array.isArray(cost)
25
+ ? (cost.find((item) => isRecord(item) && !isRecord(item.tier)) || cost.find((item) => isRecord(item)))
26
+ : (isRecord(cost) ? cost : null);
27
+ if (!isRecord(entry)) return undefined;
28
+ const projected = Object.create(null);
29
+ if (Number.isFinite(entry.input)) projected.input = entry.input;
30
+ if (Number.isFinite(entry.output)) projected.output = entry.output;
31
+ return Object.keys(projected).length > 0 ? projected : undefined;
32
+ };
33
+
49
34
  // Compose official v2 provider.list + model.list into the SDK catalog shape
50
35
  // consumed by toSmallModelCatalog. Missing arrays are failure, not empty success.
51
36
  const composeV2ProviderCatalogSource = (providers, models) => {
@@ -55,9 +40,11 @@ const composeV2ProviderCatalogSource = (providers, models) => {
55
40
  for (const model of models) {
56
41
  if (!isRecord(model)) continue;
57
42
  const providerID = typeof model.providerID === 'string' ? model.providerID : '';
58
- const modelID = typeof model.modelID === 'string' && model.modelID
59
- ? model.modelID
60
- : (typeof model.id === 'string' ? model.id : '');
43
+ // ModelInfo.id is the external model id used in generate/session refs.
44
+ // ModelInfo.modelID is a separate internal field and must not be preferred.
45
+ const modelID = typeof model.id === 'string' && model.id
46
+ ? model.id
47
+ : (typeof model.modelID === 'string' ? model.modelID : '');
61
48
  if (!providerID || !modelID) continue;
62
49
  if (!modelsByProvider.has(providerID)) modelsByProvider.set(providerID, Object.create(null));
63
50
  const entry = { id: modelID };
@@ -68,6 +55,8 @@ const composeV2ProviderCatalogSource = (providers, models) => {
68
55
  if (Number.isFinite(model.limit.output)) limit.output = model.limit.output;
69
56
  if (Object.keys(limit).length > 0) entry.limit = limit;
70
57
  }
58
+ const cost = projectCatalogCost(model.cost);
59
+ if (cost) entry.cost = cost;
71
60
  if (isRecord(model.api) && typeof model.api.url === 'string' && model.api.url.trim()) {
72
61
  entry.api = { url: model.api.url.trim() };
73
62
  }
@@ -166,7 +155,7 @@ export function createModelCatalogLoader({
166
155
  timeoutMs = CATALOG_TIMEOUT_MS,
167
156
  fetchImpl = globalThis.fetch.bind(globalThis),
168
157
  }) {
169
- /** @type {Map<string, { catalog: object, cachedAt: number, source: string, inflight: Promise<object> | null }>} */
158
+ /** @type {Map<string, { catalog: object | null, cachedAt: number, inflight: Promise<object> | null }>} */
170
159
  const buckets = new Map();
171
160
 
172
161
  const fetchOpenCodeCatalog = async (directory) => {
@@ -174,7 +163,7 @@ export function createModelCatalogLoader({
174
163
  const client = OpenCode.make({
175
164
  baseUrl,
176
165
  headers: getOpenCodeAuthHeaders(),
177
- fetch: (request) => fetchImpl(request, { signal: AbortSignal.timeout(timeoutMs) }),
166
+ fetch: (request, init) => fetchImpl(request, { ...init, signal: AbortSignal.timeout(timeoutMs) }),
178
167
  });
179
168
  const location = directory ? { directory } : undefined;
180
169
  const request = location ? { location } : undefined;
@@ -194,8 +183,8 @@ export function createModelCatalogLoader({
194
183
  };
195
184
 
196
185
  /**
197
- * Load (or serve cached) directory-scoped catalog.
198
- * OpenCode failure → explicit minimal fallback (never an authoritative empty map).
186
+ * Load (or serve cached) directory-scoped catalog. Failures are not cached
187
+ * and reject with statusCode 502 so callers never resolve against a guess.
199
188
  */
200
189
  const getModelCatalog = async (directory) => {
201
190
  const key = normalizeDirectoryKey(directory);
@@ -206,7 +195,7 @@ export function createModelCatalogLoader({
206
195
  }
207
196
 
208
197
  if (!bucket) {
209
- bucket = { catalog: null, cachedAt: 0, source: 'none', inflight: null };
198
+ bucket = { catalog: null, cachedAt: 0, inflight: null };
210
199
  buckets.set(key, bucket);
211
200
  }
212
201
 
@@ -216,18 +205,12 @@ export function createModelCatalogLoader({
216
205
  const catalog = await fetchOpenCodeCatalog(key || undefined);
217
206
  bucket.catalog = catalog;
218
207
  bucket.cachedAt = Date.now();
219
- bucket.source = 'opencode';
220
208
  return catalog;
221
209
  } catch (error) {
222
- // Explicit failure path: minimal hardcoded candidates only.
223
- console.warn(
224
- '[small-model] OpenCode provider catalog failed; using minimal fallback:',
225
- error?.message || error,
210
+ throw Object.assign(
211
+ new Error(`OpenCode provider catalog is unavailable (${error?.message || error})`),
212
+ { statusCode: 502 },
226
213
  );
227
- bucket.catalog = MINIMAL_FALLBACK_CATALOG;
228
- bucket.cachedAt = Date.now();
229
- bucket.source = 'fallback';
230
- return MINIMAL_FALLBACK_CATALOG;
231
214
  } finally {
232
215
  bucket.inflight = null;
233
216
  }
@@ -242,8 +225,5 @@ export function createModelCatalogLoader({
242
225
  getCatalogProvider,
243
226
  /** @internal test helpers */
244
227
  _normalizeDirectoryKey: normalizeDirectoryKey,
245
- _minimalFallbackCatalog: MINIMAL_FALLBACK_CATALOG,
246
228
  };
247
229
  }
248
-
249
- export { MINIMAL_FALLBACK_CATALOG };
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { createModelCatalogLoader, toSmallModelCatalog, MINIMAL_FALLBACK_CATALOG } from './catalog.js';
2
+ import { createModelCatalogLoader, toSmallModelCatalog } from './catalog.js';
3
3
 
4
4
  describe('toSmallModelCatalog', () => {
5
5
  it('keeps family, release_date, limit, cost, model.api.url, and provider name', () => {
@@ -45,13 +45,15 @@ describe('toSmallModelCatalog', () => {
45
45
  });
46
46
 
47
47
  describe('createModelCatalogLoader', () => {
48
- it('uses explicit minimal fallback when OpenCode is unreachable (not empty catalog)', async () => {
48
+ it('rejects with 502 when OpenCode is unreachable and does not cache the failure', async () => {
49
+ let calls = 0;
49
50
  const loader = createModelCatalogLoader({
50
51
  buildOpenCodeUrl: () => 'http://127.0.0.1:9/',
51
52
  getOpenCodeAuthHeaders: () => ({}),
52
53
  ttlMs: 30_000,
53
54
  timeoutMs: 50,
54
55
  fetchImpl: async () => {
56
+ calls += 1;
55
57
  throw new Error('network down');
56
58
  },
57
59
  });
@@ -59,13 +61,34 @@ describe('createModelCatalogLoader', () => {
59
61
  const a = loader.getModelCatalog('/proj');
60
62
  const b = loader.getModelCatalog('/proj/');
61
63
  // Single-flight + directory key normalization: same bucket for trailing slash.
62
- const [catalogA, catalogB] = await Promise.all([a, b]);
63
- expect(catalogA).toBe(MINIMAL_FALLBACK_CATALOG);
64
- expect(catalogB).toBe(MINIMAL_FALLBACK_CATALOG);
65
- expect(catalogA.google?.models?.['gemini-2.5-flash']?.family).toBe('gemini-flash');
66
- expect(catalogA.anthropic?.models?.['claude-haiku-4-5']?.family).toBe('claude-haiku');
67
- // Must not masquerade as authoritative empty success.
68
- expect(Object.keys(catalogA).length).toBeGreaterThan(0);
64
+ const results = await Promise.allSettled([a, b]);
65
+ for (const result of results) {
66
+ expect(result.status).toBe('rejected');
67
+ expect(result.reason.statusCode).toBe(502);
68
+ expect(result.reason.message).toMatch(/provider catalog is unavailable/);
69
+ }
70
+ const callsAfterFirst = calls;
71
+ await expect(loader.getModelCatalog('/proj')).rejects.toMatchObject({ statusCode: 502 });
72
+ expect(calls).toBeGreaterThan(callsAfterFirst);
73
+ });
74
+
75
+ it('forwards OpenCode auth headers through the fetch wrapper', async () => {
76
+ const seen = [];
77
+ const loader = createModelCatalogLoader({
78
+ buildOpenCodeUrl: () => 'http://127.0.0.1:4096/',
79
+ getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic test' }),
80
+ fetchImpl: async (url, init) => {
81
+ seen.push(new Headers(init?.headers).get('authorization'));
82
+ return new Response(JSON.stringify({ data: [] }), {
83
+ status: 200,
84
+ headers: { 'content-type': 'application/json' },
85
+ });
86
+ },
87
+ });
88
+
89
+ await loader.getModelCatalog('/proj');
90
+ expect(seen.length).toBeGreaterThan(0);
91
+ expect(seen.every((value) => value === 'Basic test')).toBe(true);
69
92
  });
70
93
 
71
94
  it('composes official v2 provider.list + model.list into the small-model catalog', async () => {
@@ -99,7 +122,6 @@ describe('createModelCatalogLoader', () => {
99
122
  });
100
123
 
101
124
  const catalog = await loader.getModelCatalog('/proj');
102
- expect(catalog).not.toBe(MINIMAL_FALLBACK_CATALOG);
103
125
  expect(catalog.google).toEqual({
104
126
  id: 'google',
105
127
  name: 'Google',
@@ -115,6 +137,39 @@ describe('createModelCatalogLoader', () => {
115
137
  });
116
138
  });
117
139
 
140
+ it('keys models by ModelInfo.id and keeps the base tier of array cost', async () => {
141
+ const loader = createModelCatalogLoader({
142
+ buildOpenCodeUrl: () => 'http://127.0.0.1:4096/',
143
+ getOpenCodeAuthHeaders: () => ({}),
144
+ fetchImpl: async (url) => {
145
+ const href = String(url);
146
+ const data = href.includes('/api/model')
147
+ ? [{
148
+ id: 'gpt-5.4-mini',
149
+ modelID: 'internal-pack',
150
+ providerID: 'openai',
151
+ name: 'GPT-5.4 Mini',
152
+ cost: [
153
+ { tier: { type: 'context', size: 200000 }, input: 9, output: 9 },
154
+ { input: 1, output: 2 },
155
+ ],
156
+ }]
157
+ : [{ id: 'openai', name: 'OpenAI', package: '@ai-sdk/openai' }];
158
+ return new Response(JSON.stringify({ data }), {
159
+ status: 200,
160
+ headers: { 'content-type': 'application/json' },
161
+ });
162
+ },
163
+ });
164
+
165
+ const catalog = await loader.getModelCatalog('/proj');
166
+ expect(catalog.openai?.models?.['gpt-5.4-mini']).toEqual({
167
+ id: 'gpt-5.4-mini',
168
+ cost: { input: 1, output: 2 },
169
+ });
170
+ expect(catalog.openai?.models?.['internal-pack']).toBeUndefined();
171
+ });
172
+
118
173
  it('normalizes directory keys so trailing slashes share a bucket', () => {
119
174
  const loader = createModelCatalogLoader({
120
175
  buildOpenCodeUrl: () => 'http://127.0.0.1:9/',
@@ -1,5 +1,9 @@
1
1
  const TEST_TIMEOUT_MS = 15_000;
2
2
  const MODELS_TIMEOUT_MS = 8_000;
3
+ const GENERATE_TIMEOUT_MS = 60_000;
4
+ // Generous default: thinking models that can't be switched off spend part of
5
+ // this budget on reasoning before the actual answer.
6
+ const DEFAULT_MAX_OUTPUT_TOKENS = 4_000;
3
7
 
4
8
  const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
5
9
 
@@ -96,6 +100,70 @@ export async function testCustomSummaryApi({
96
100
  }
97
101
  }
98
102
 
103
+ /**
104
+ * Summary AI custom mode: one non-streaming chat completion against the
105
+ * user's own OpenAI-compatible endpoint. The token never leaves this process.
106
+ */
107
+ export async function generateCustomSummaryText({
108
+ baseURL,
109
+ apiToken,
110
+ modelID,
111
+ prompt,
112
+ system,
113
+ maxOutputTokens,
114
+ fetchImpl = globalThis.fetch.bind(globalThis),
115
+ }) {
116
+ const parsedBaseURL = parseCustomApiBaseURL(baseURL);
117
+ if (!parsedBaseURL) {
118
+ throw Object.assign(new Error('Custom summary API Base URL is invalid'), { statusCode: 400 });
119
+ }
120
+ const response = await fetchImpl(`${parsedBaseURL}/chat/completions`, {
121
+ method: 'POST',
122
+ headers: {
123
+ 'Content-Type': 'application/json',
124
+ Accept: 'application/json',
125
+ Authorization: `Bearer ${apiToken}`,
126
+ },
127
+ body: JSON.stringify({
128
+ model: modelID,
129
+ messages: [
130
+ ...(system ? [{ role: 'system', content: system }] : []),
131
+ { role: 'user', content: prompt },
132
+ ],
133
+ max_tokens: Number(maxOutputTokens) > 0 ? Number(maxOutputTokens) : DEFAULT_MAX_OUTPUT_TOKENS,
134
+ stream: false,
135
+ }),
136
+ signal: AbortSignal.timeout(GENERATE_TIMEOUT_MS),
137
+ });
138
+ const bodyText = await response.text().catch(() => '');
139
+ if (!response.ok) {
140
+ const snippet = bodyText ? `: ${bodyText.slice(0, 300)}` : '';
141
+ throw new Error(`Custom summary API request failed with ${response.status}${snippet}`);
142
+ }
143
+ const payload = parseJson(bodyText);
144
+ const message = payload?.choices?.[0]?.message;
145
+ // Providers disagree on the content shape: plain string, an array of typed
146
+ // parts, or (thinking models) empty content with the budget spent on
147
+ // reasoning_content.
148
+ let text = '';
149
+ if (typeof message?.content === 'string') {
150
+ text = message.content;
151
+ } else if (Array.isArray(message?.content)) {
152
+ text = message.content.map((part) => (typeof part?.text === 'string' ? part.text : '')).join('');
153
+ }
154
+ if (!text.trim() && typeof message?.reasoning_content === 'string' && message.reasoning_content.trim()) {
155
+ const finishReason = payload?.choices?.[0]?.finish_reason;
156
+ throw new Error(
157
+ 'Custom summary API spent the output budget on reasoning and returned no answer'
158
+ + (finishReason ? ` (finish_reason: ${finishReason})` : ''),
159
+ );
160
+ }
161
+ if (!text.trim()) {
162
+ throw new Error('Custom summary API returned no message content');
163
+ }
164
+ return text;
165
+ }
166
+
99
167
  export async function listCustomSummaryModels({
100
168
  baseURL,
101
169
  apiToken,
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import {
3
3
  classifyCustomSummaryApiFailure,
4
+ generateCustomSummaryText,
4
5
  listCustomSummaryModels,
5
6
  parseCustomApiBaseURL,
6
7
  testCustomSummaryApi,
@@ -99,3 +100,66 @@ describe('listCustomSummaryModels', () => {
99
100
  })).toEqual([]);
100
101
  });
101
102
  });
103
+
104
+ describe('generateCustomSummaryText', () => {
105
+ const completion = (message, extra = {}) => new Response(JSON.stringify({
106
+ choices: [{ message, ...extra }],
107
+ }), { status: 200, headers: { 'content-type': 'application/json' } });
108
+
109
+ it('posts an OpenAI-compatible chat completion and returns the text', async () => {
110
+ const fetchImpl = vi.fn(async () => completion({ role: 'assistant', content: 'feat: add retries' }));
111
+ const text = await generateCustomSummaryText({
112
+ baseURL: 'https://summary.example.test/v1/',
113
+ apiToken: 'token',
114
+ modelID: 'summary-model',
115
+ prompt: 'Diff',
116
+ system: 'Return a commit subject.',
117
+ maxOutputTokens: 64,
118
+ fetchImpl,
119
+ });
120
+ expect(text).toBe('feat: add retries');
121
+ const [url, init] = fetchImpl.mock.calls[0];
122
+ expect(url).toBe('https://summary.example.test/v1/chat/completions');
123
+ expect(init.method).toBe('POST');
124
+ expect(JSON.parse(init.body)).toEqual({
125
+ model: 'summary-model',
126
+ messages: [
127
+ { role: 'system', content: 'Return a commit subject.' },
128
+ { role: 'user', content: 'Diff' },
129
+ ],
130
+ max_tokens: 64,
131
+ stream: false,
132
+ });
133
+ });
134
+
135
+ it('joins array content parts', async () => {
136
+ const fetchImpl = vi.fn(async () => completion({ content: [{ type: 'text', text: 'a' }, { type: 'text', text: 'b' }] }));
137
+ await expect(generateCustomSummaryText({
138
+ baseURL: 'https://summary.example.test/v1', apiToken: 't', modelID: 'm', prompt: 'p', fetchImpl,
139
+ })).resolves.toBe('ab');
140
+ });
141
+
142
+ it('rejects invalid base URLs before any request', async () => {
143
+ const fetchImpl = vi.fn();
144
+ await expect(generateCustomSummaryText({
145
+ baseURL: 'ftp://nope', apiToken: 't', modelID: 'm', prompt: 'p', fetchImpl,
146
+ })).rejects.toMatchObject({ statusCode: 400 });
147
+ expect(fetchImpl).not.toHaveBeenCalled();
148
+ });
149
+
150
+ it('surfaces HTTP failures, reasoning-only replies, and empty content as errors', async () => {
151
+ const input = { baseURL: 'https://summary.example.test/v1', apiToken: 't', modelID: 'm', prompt: 'p' };
152
+ await expect(generateCustomSummaryText({
153
+ ...input,
154
+ fetchImpl: async () => new Response('rate limited', { status: 429 }),
155
+ })).rejects.toThrow(/failed with 429/);
156
+ await expect(generateCustomSummaryText({
157
+ ...input,
158
+ fetchImpl: async () => completion({ content: '', reasoning_content: 'thinking…' }, { finish_reason: 'length' }),
159
+ })).rejects.toThrow(/reasoning.*finish_reason: length/);
160
+ await expect(generateCustomSummaryText({
161
+ ...input,
162
+ fetchImpl: async () => completion({ content: '' }),
163
+ })).rejects.toThrow(/no message content/);
164
+ });
165
+ });