@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.3

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 (311) hide show
  1. package/README.md +13 -7
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +179 -0
  6. package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
  7. package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
  8. package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
  10. package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
  13. package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
  15. package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
  16. package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
  17. package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
  20. package/dist/assets/SettingsView-BRxELjow.js +25 -0
  21. package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
  22. package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
  24. package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
  25. package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
  26. package/dist/assets/collapsible-DXbDynN4.js +3 -0
  27. package/dist/assets/context-menu-CqolsLCm.js +1 -0
  28. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
  29. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
  30. package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
  31. package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
  32. package/dist/assets/es-CN3E_zXj.js +15 -0
  33. package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
  34. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
  35. package/dist/assets/fr-9zZvQZD2.js +13 -0
  36. package/dist/assets/gitApi-CM42qUed.js +10 -0
  37. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
  38. package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
  39. package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
  40. package/dist/assets/ja-Dnjk_82N.js +15 -0
  41. package/dist/assets/ko-BxCEE99C.js +15 -0
  42. package/dist/assets/main-BILgBI3B.js +169 -0
  43. package/dist/assets/main-BeveiW6F.js +2 -0
  44. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  45. package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
  46. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  47. package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
  48. package/dist/assets/miniChat-BgrzM3u7.js +2 -0
  49. package/dist/assets/mobile-DtYoQE8C.js +2 -0
  50. package/dist/assets/multirun-jy8dEI96.js +4 -0
  51. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
  52. package/dist/assets/pl-CNNsWnPM.js +15 -0
  53. package/dist/assets/platform-BQghHRfp.js +6 -0
  54. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  55. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
  56. package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
  57. package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
  58. package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
  59. package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
  60. package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
  61. package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
  62. package/dist/assets/src-CLN7Z-zx.css +2 -0
  63. package/dist/assets/store-DLCiZaA6.js +15 -0
  64. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
  65. package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
  66. package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
  67. package/dist/assets/uk-rwQrxeYM.js +15 -0
  68. package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
  69. package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
  70. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  71. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
  72. package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
  73. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
  74. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
  75. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  76. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +2 -0
  77. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  78. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  79. package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.js} +2 -2
  80. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  81. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  82. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  83. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  84. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  85. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  86. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  87. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  88. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  89. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  90. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  91. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  92. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  93. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  94. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  95. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  96. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  97. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  98. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  99. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  100. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  101. package/dist/index.html +13 -13
  102. package/dist/mini-chat.html +13 -13
  103. package/dist/mobile.html +13 -13
  104. package/package.json +2 -2
  105. package/server/index.js +392 -20
  106. package/server/lib/assistants/DOCUMENTATION.md +26 -21
  107. package/server/lib/assistants/assign.js +2 -1
  108. package/server/lib/assistants/contact-store.js +37 -19
  109. package/server/lib/assistants/contracts.js +11 -1
  110. package/server/lib/assistants/projection-history.test.js +454 -0
  111. package/server/lib/assistants/routes.js +11 -5
  112. package/server/lib/assistants/service.js +850 -294
  113. package/server/lib/assistants/service.test.js +588 -274
  114. package/server/lib/conversations/DOCUMENTATION.md +5 -5
  115. package/server/lib/conversations/service.js +87 -57
  116. package/server/lib/conversations/service.test.js +132 -100
  117. package/server/lib/conversations/validation.js +11 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +51 -5
  120. package/server/lib/event-stream/runtime.js +57 -20
  121. package/server/lib/event-stream/runtime.test.js +112 -0
  122. package/server/lib/llm/DOCUMENTATION.md +95 -84
  123. package/server/lib/llm/catalog.js +111 -45
  124. package/server/lib/llm/catalog.test.js +121 -59
  125. package/server/lib/llm/completions.js +41 -12
  126. package/server/lib/llm/completions.test.js +45 -4
  127. package/server/lib/llm/generate.js +516 -453
  128. package/server/lib/llm/generate.test.js +705 -484
  129. package/server/lib/llm/routes.js +8 -4
  130. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  131. package/server/lib/message-queue/opencode-adapter.js +191 -104
  132. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  133. package/server/lib/message-queue/runtime.js +15 -2
  134. package/server/lib/message-queue/runtime.test.js +6 -6
  135. package/server/lib/message-queue/worker.js +7 -1
  136. package/server/lib/notifications/runtime.js +5 -0
  137. package/server/lib/opencode/DOCUMENTATION.md +103 -16
  138. package/server/lib/opencode/agent-document.d.ts +28 -0
  139. package/server/lib/opencode/agent-document.js +280 -0
  140. package/server/lib/opencode/agent-document.test.js +137 -0
  141. package/server/lib/opencode/agents.js +96 -0
  142. package/server/lib/opencode/auth-state-runtime.js +5 -0
  143. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  144. package/server/lib/opencode/config-entity-routes.js +111 -102
  145. package/server/lib/opencode/config-entity-routes.test.js +123 -10
  146. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  147. package/server/lib/opencode/ensure-cli.js +238 -0
  148. package/server/lib/opencode/ensure-cli.test.js +119 -0
  149. package/server/lib/opencode/env-runtime.js +208 -111
  150. package/server/lib/opencode/env-runtime.test.js +211 -14
  151. package/server/lib/opencode/feature-routes-runtime.js +80 -5
  152. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  153. package/server/lib/opencode/http-server-connections.js +98 -0
  154. package/server/lib/opencode/http-server-connections.test.js +321 -0
  155. package/server/lib/opencode/lifecycle.js +539 -95
  156. package/server/lib/opencode/lifecycle.test.js +698 -6
  157. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  158. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  159. package/server/lib/opencode/network-runtime.js +28 -16
  160. package/server/lib/opencode/network-runtime.test.js +68 -4
  161. package/server/lib/opencode/openchamber-routes.js +7 -1
  162. package/server/lib/opencode/opencode2-pin.js +139 -0
  163. package/server/lib/opencode/opencode2-pin.test.js +88 -0
  164. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
  165. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  166. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  167. package/server/lib/opencode/plugin-compatibility.js +15 -0
  168. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  169. package/server/lib/opencode/plugin-routes.js +3 -18
  170. package/server/lib/opencode/plugin-routes.test.js +9 -9
  171. package/server/lib/opencode/plugins.js +4 -3
  172. package/server/lib/opencode/plugins.test.js +12 -0
  173. package/server/lib/opencode/provider-catalog.js +58 -12
  174. package/server/lib/opencode/provider-catalog.test.js +48 -0
  175. package/server/lib/opencode/proxy.js +368 -28
  176. package/server/lib/opencode/proxy.test.js +338 -0
  177. package/server/lib/opencode/routes.js +303 -76
  178. package/server/lib/opencode/routes.test.js +315 -50
  179. package/server/lib/opencode/runtime-contract.js +401 -0
  180. package/server/lib/opencode/runtime-contract.test.js +214 -0
  181. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  182. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  183. package/server/lib/opencode/server-utils-runtime.js +35 -0
  184. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  185. package/server/lib/opencode/shared-service.js +147 -0
  186. package/server/lib/opencode/shared-service.test.js +93 -0
  187. package/server/lib/opencode/shared.js +3 -3
  188. package/server/lib/opencode/shutdown-runtime.js +15 -6
  189. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  190. package/server/lib/opencode/skill-routes.js +26 -40
  191. package/server/lib/opencode/skill-routes.test.js +71 -7
  192. package/server/lib/opencode/skills.test.js +15 -0
  193. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  194. package/server/lib/opencode/static-routes-runtime.js +3 -1
  195. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  196. package/server/lib/opencode/v1-migration-gate.js +157 -0
  197. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  198. package/server/lib/opencode/v2-client.js +60 -0
  199. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  200. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  201. package/server/lib/opencode/watcher.js +2 -1
  202. package/server/lib/opencode/watcher.test.js +1 -1
  203. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  204. package/server/lib/permission-auto-accept/runtime.js +14 -7
  205. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  206. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  207. package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
  208. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  209. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  210. package/server/lib/scheduled-tasks/routes.js +27 -0
  211. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  212. package/server/lib/scheduled-tasks/runtime.js +381 -310
  213. package/server/lib/scheduled-tasks/runtime.test.js +600 -309
  214. package/server/lib/session-goal/DOCUMENTATION.md +173 -22
  215. package/server/lib/session-goal/capability.js +57 -0
  216. package/server/lib/session-goal/capability.test.js +33 -0
  217. package/server/lib/session-goal/routes.js +240 -2
  218. package/server/lib/session-goal/routes.test.js +211 -0
  219. package/server/lib/session-goal/runtime.js +1617 -113
  220. package/server/lib/session-goal/runtime.test.js +1500 -70
  221. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  222. package/server/lib/session-index/event-ingest.js +89 -16
  223. package/server/lib/session-index/event-ingest.test.js +73 -0
  224. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  225. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  226. package/server/lib/session-index/routes.js +63 -10
  227. package/server/lib/session-index/routes.test.js +8 -8
  228. package/server/lib/session-index/service.js +7 -1
  229. package/server/lib/session-index/sync-runtime.js +171 -41
  230. package/server/lib/session-index/sync-runtime.test.js +164 -9
  231. package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
  232. package/server/lib/session-metadata/routes.js +193 -0
  233. package/server/lib/session-metadata/routes.test.js +156 -0
  234. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  235. package/server/lib/session-metadata/session-archive.js +654 -0
  236. package/server/lib/session-metadata/session-archive.test.js +359 -0
  237. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  238. package/server/lib/session-metadata/session-metadata-store.js +375 -0
  239. package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
  240. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  241. package/server/lib/session-metadata/session-projection.js +255 -0
  242. package/server/lib/session-metadata/session-projection.test.js +64 -0
  243. package/server/lib/session-metadata/system-session.js +55 -0
  244. package/server/lib/session-metadata/system-session.test.js +55 -0
  245. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  246. package/server/lib/session-title/native-runtime.test.js +167 -0
  247. package/server/lib/session-title/runtime.js +67 -42
  248. package/server/lib/session-title/session-access.js +46 -0
  249. package/server/lib/session-title/session-access.test.js +91 -0
  250. package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
  251. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  252. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  253. package/server/lib/session-turn-pages/routes.js +123 -75
  254. package/server/lib/session-turn-pages/routes.test.js +566 -0
  255. package/server/lib/session-turn-pages/service.js +6 -2
  256. package/server/lib/session-turn-pages/service.test.js +34 -0
  257. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  258. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  259. package/server/lib/small-model/DOCUMENTATION.md +101 -146
  260. package/server/lib/small-model/catalog.js +84 -54
  261. package/server/lib/small-model/catalog.test.js +111 -9
  262. package/server/lib/small-model/custom-api.js +68 -0
  263. package/server/lib/small-model/custom-api.test.js +64 -0
  264. package/server/lib/small-model/index.js +125 -241
  265. package/server/lib/small-model/resolve.js +35 -120
  266. package/server/lib/small-model/resolve.test.js +20 -110
  267. package/server/lib/small-model/routes.js +1 -3
  268. package/server/lib/small-model/summary-settings.test.js +157 -253
  269. package/server/opencode-proxy.test.js +63 -12
  270. package/dist/assets/AssistantView-BUveAgc4.js +0 -3
  271. package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
  272. package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
  273. package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
  274. package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
  275. package/dist/assets/SettingsView-PkXIavr7.js +0 -25
  276. package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
  277. package/dist/assets/collapsible-D5Phaykh.js +0 -1
  278. package/dist/assets/context-menu-CubSDW2g.js +0 -1
  279. package/dist/assets/es-DA3MqOo0.js +0 -15
  280. package/dist/assets/fr-Dbz0CdIA.js +0 -13
  281. package/dist/assets/gitApi-DXyC4MiV.js +0 -7
  282. package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
  283. package/dist/assets/ja-DumanVQA.js +0 -15
  284. package/dist/assets/ko-C0IkiUjQ.js +0 -15
  285. package/dist/assets/main-BPS5ylib.js +0 -2
  286. package/dist/assets/main-Cpd9VWYs.js +0 -169
  287. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  288. package/dist/assets/miniChat-B8_KLWiY.js +0 -2
  289. package/dist/assets/mobile-CtktKrXn.js +0 -2
  290. package/dist/assets/multirun-CYfP9Cfm.js +0 -4
  291. package/dist/assets/pl-B9No6MQp.js +0 -15
  292. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  293. package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
  294. package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
  295. package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
  296. package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
  297. package/dist/assets/src-rS3zCcsC.css +0 -2
  298. package/dist/assets/store-5B_LDnLM.js +0 -15
  299. package/dist/assets/uk-C_1KkqyP.js +0 -15
  300. package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
  301. package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
  302. package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
  303. package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
  304. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  305. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  306. package/dist/assets/zh-CN-uWILgBdt.js +0 -15
  307. package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
  308. package/server/lib/small-model/call.js +0 -427
  309. package/server/lib/small-model/call.test.js +0 -382
  310. package/server/lib/small-model/opencode-session.js +0 -339
  311. package/server/lib/small-model/opencode-session.test.js +0 -177
@@ -1,41 +1,14 @@
1
- import { createOpencodeClient } from '@opencode-ai/sdk/v2';
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,67 @@ 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
+
34
+ // Compose official v2 provider.list + model.list into the SDK catalog shape
35
+ // consumed by toSmallModelCatalog. Missing arrays are failure, not empty success.
36
+ const composeV2ProviderCatalogSource = (providers, models) => {
37
+ if (!Array.isArray(providers) || !Array.isArray(models)) return null;
38
+
39
+ const modelsByProvider = new Map();
40
+ for (const model of models) {
41
+ if (!isRecord(model)) continue;
42
+ const providerID = typeof model.providerID === 'string' ? model.providerID : '';
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 : '');
48
+ if (!providerID || !modelID) continue;
49
+ if (!modelsByProvider.has(providerID)) modelsByProvider.set(providerID, Object.create(null));
50
+ const entry = { id: modelID };
51
+ if (typeof model.family === 'string' && model.family) entry.family = model.family;
52
+ if (isRecord(model.limit)) {
53
+ const limit = Object.create(null);
54
+ if (Number.isFinite(model.limit.context)) limit.context = model.limit.context;
55
+ if (Number.isFinite(model.limit.output)) limit.output = model.limit.output;
56
+ if (Object.keys(limit).length > 0) entry.limit = limit;
57
+ }
58
+ const cost = projectCatalogCost(model.cost);
59
+ if (cost) entry.cost = cost;
60
+ if (isRecord(model.api) && typeof model.api.url === 'string' && model.api.url.trim()) {
61
+ entry.api = { url: model.api.url.trim() };
62
+ }
63
+ if (typeof model.time?.released === 'number' && Number.isFinite(model.time.released)) {
64
+ entry.release_date = new Date(model.time.released).toISOString().slice(0, 10);
65
+ } else if (typeof model.release_date === 'string' && model.release_date) {
66
+ entry.release_date = model.release_date;
67
+ }
68
+ modelsByProvider.get(providerID)[modelID] = entry;
69
+ }
70
+
71
+ const catalogProviders = [];
72
+ for (const provider of providers) {
73
+ if (!isRecord(provider) || typeof provider.id !== 'string' || !provider.id) continue;
74
+ catalogProviders.push({
75
+ id: provider.id,
76
+ name: typeof provider.name === 'string' && provider.name ? provider.name : provider.id,
77
+ models: modelsByProvider.get(provider.id) || Object.create(null),
78
+ });
79
+ }
80
+ return { providers: catalogProviders };
81
+ };
82
+
49
83
  /**
50
84
  * Convert the official SDK provider list into the internal small-model catalog
51
85
  * shape. Keeps family / release_date / limit / cost.input|output /
@@ -121,22 +155,27 @@ export function createModelCatalogLoader({
121
155
  timeoutMs = CATALOG_TIMEOUT_MS,
122
156
  fetchImpl = globalThis.fetch.bind(globalThis),
123
157
  }) {
124
- /** @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 }>} */
125
159
  const buckets = new Map();
126
160
 
127
161
  const fetchOpenCodeCatalog = async (directory) => {
128
162
  const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
129
- const client = createOpencodeClient({
163
+ const client = OpenCode.make({
130
164
  baseUrl,
131
- ...(directory ? { directory } : {}),
132
165
  headers: getOpenCodeAuthHeaders(),
133
- fetch: (request) => fetchImpl(request, { signal: AbortSignal.timeout(timeoutMs) }),
166
+ fetch: (request, init) => fetchImpl(request, { ...init, signal: AbortSignal.timeout(timeoutMs) }),
134
167
  });
135
- const response = await client.config.providers(directory ? { directory } : {});
136
- if (response?.error || response?.data === undefined) {
168
+ const location = directory ? { directory } : undefined;
169
+ const request = location ? { location } : undefined;
170
+ const [providersResult, modelsResult] = await Promise.all([
171
+ client.provider.list(request),
172
+ client.model.list(request),
173
+ ]);
174
+ const source = composeV2ProviderCatalogSource(providersResult?.data, modelsResult?.data);
175
+ if (!source) {
137
176
  throw new Error('OpenCode provider catalog is unavailable');
138
177
  }
139
- const catalog = toSmallModelCatalog(response.data);
178
+ const catalog = toSmallModelCatalog(source);
140
179
  if (!catalog) {
141
180
  throw new Error('OpenCode provider catalog returned an unexpected payload');
142
181
  }
@@ -144,8 +183,8 @@ export function createModelCatalogLoader({
144
183
  };
145
184
 
146
185
  /**
147
- * Load (or serve cached) directory-scoped catalog.
148
- * 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.
149
188
  */
150
189
  const getModelCatalog = async (directory) => {
151
190
  const key = normalizeDirectoryKey(directory);
@@ -156,7 +195,7 @@ export function createModelCatalogLoader({
156
195
  }
157
196
 
158
197
  if (!bucket) {
159
- bucket = { catalog: null, cachedAt: 0, source: 'none', inflight: null };
198
+ bucket = { catalog: null, cachedAt: 0, inflight: null };
160
199
  buckets.set(key, bucket);
161
200
  }
162
201
 
@@ -166,18 +205,12 @@ export function createModelCatalogLoader({
166
205
  const catalog = await fetchOpenCodeCatalog(key || undefined);
167
206
  bucket.catalog = catalog;
168
207
  bucket.cachedAt = Date.now();
169
- bucket.source = 'opencode';
170
208
  return catalog;
171
209
  } catch (error) {
172
- // Explicit failure path: minimal hardcoded candidates only.
173
- console.warn(
174
- '[small-model] OpenCode provider catalog failed; using minimal fallback:',
175
- error?.message || error,
210
+ throw Object.assign(
211
+ new Error(`OpenCode provider catalog is unavailable (${error?.message || error})`),
212
+ { statusCode: 502 },
176
213
  );
177
- bucket.catalog = MINIMAL_FALLBACK_CATALOG;
178
- bucket.cachedAt = Date.now();
179
- bucket.source = 'fallback';
180
- return MINIMAL_FALLBACK_CATALOG;
181
214
  } finally {
182
215
  bucket.inflight = null;
183
216
  }
@@ -192,8 +225,5 @@ export function createModelCatalogLoader({
192
225
  getCatalogProvider,
193
226
  /** @internal test helpers */
194
227
  _normalizeDirectoryKey: normalizeDirectoryKey,
195
- _minimalFallbackCatalog: MINIMAL_FALLBACK_CATALOG,
196
228
  };
197
229
  }
198
-
199
- 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,113 @@ 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);
92
+ });
93
+
94
+ it('composes official v2 provider.list + model.list into the small-model catalog', async () => {
95
+ const loader = createModelCatalogLoader({
96
+ buildOpenCodeUrl: () => 'http://127.0.0.1:4096/',
97
+ getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic test' }),
98
+ fetchImpl: async (url) => {
99
+ const href = String(url);
100
+ const data = href.includes('/api/model')
101
+ ? [{
102
+ id: 'gemini-2.5-flash',
103
+ modelID: 'gemini-2.5-flash',
104
+ providerID: 'google',
105
+ family: 'gemini-flash',
106
+ name: 'Gemini 2.5 Flash',
107
+ capabilities: {},
108
+ variants: [],
109
+ time: { released: Date.parse('2025-06-01T00:00:00Z') },
110
+ cost: [],
111
+ status: 'active',
112
+ enabled: true,
113
+ limit: { context: 1_000_000, output: 8192 },
114
+ api: { url: 'https://generativelanguage.googleapis.com' },
115
+ }]
116
+ : [{ id: 'google', name: 'Google', package: '@ai-sdk/google' }];
117
+ return new Response(JSON.stringify({
118
+ location: { directory: '/proj', project: { id: 'p', directory: '/proj', canonical: '/proj' } },
119
+ data,
120
+ }), { status: 200, headers: { 'content-type': 'application/json' } });
121
+ },
122
+ });
123
+
124
+ const catalog = await loader.getModelCatalog('/proj');
125
+ expect(catalog.google).toEqual({
126
+ id: 'google',
127
+ name: 'Google',
128
+ models: {
129
+ 'gemini-2.5-flash': {
130
+ id: 'gemini-2.5-flash',
131
+ family: 'gemini-flash',
132
+ release_date: '2025-06-01',
133
+ limit: { context: 1_000_000, output: 8192 },
134
+ api: { url: 'https://generativelanguage.googleapis.com' },
135
+ },
136
+ },
137
+ });
138
+ });
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();
69
171
  });
70
172
 
71
173
  it('normalizes directory keys so trailing slashes share a bucket', () => {
@@ -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
+ });