@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,15 +1,12 @@
1
1
  import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { readAuthFile } from '../opencode/auth.js';
5
4
  import { readConfigLayers } from '../opencode/shared.js';
6
5
  import { OpenCode } from '@opencode/client';
7
- import { loadConnectedCatalog } from '../llm/catalog.js';
8
- import { createModelCatalogLoader, getCatalogProvider } from './catalog.js';
9
- import { resolveSmallModel, parseModelRef, isUsableAuthEntry, getAuthEntryForProvider } from './resolve.js';
10
- import { callSmallModel } from './call.js';
11
- import { generateViaOpenCodeSession, stop as stopOpenCodeSessionTemp } from './opencode-session.js';
12
- import { listCustomSummaryModels, testCustomSummaryApi } from './custom-api.js';
6
+ import { createChatCompletion } from '../llm/completions.js';
7
+ import { createModelCatalogLoader } from './catalog.js';
8
+ import { resolveSmallModel, parseModelRef } from './resolve.js';
9
+ import { generateCustomSummaryText, listCustomSummaryModels, testCustomSummaryApi } from './custom-api.js';
13
10
 
14
11
  const OPENCHAMBER_SETTINGS_FILE = path.join(
15
12
  process.env.OPENCHAMBER_DATA_DIR
@@ -68,8 +65,7 @@ const readSmallModelSettingsOverride = () => {
68
65
 
69
66
  // Rough safety clamp so a huge input never blows the model's context window.
70
67
  // Token estimate is ~4 chars/token; when the catalog has no limit for the
71
- // model (Copilot/codex utility models are not listed) a conservative default
72
- // applies.
68
+ // model (or the call goes to a custom API) a conservative default applies.
73
69
  const DEFAULT_CONTEXT_TOKENS = 64_000;
74
70
  const OUTPUT_RESERVE_TOKENS = 4_000;
75
71
 
@@ -84,106 +80,6 @@ const clampPromptToModelLimit = ({ prompt, catalog, providerID, modelID }) => {
84
80
  return { prompt: `${prompt.slice(0, maxChars)}…`, truncated: true };
85
81
  };
86
82
 
87
- // Dedicated adapters keep the direct wire-format path. Everything else
88
- // (plugin providers, credential-chain, missing api.url, …) goes through a
89
- // temporary OpenCode session so auth/endpoint rewrite stay in the runtime.
90
- const DEDICATED_DIRECT_PROVIDERS = new Set(['openai', 'anthropic', 'google', 'github-copilot']);
91
-
92
- // Mirrors pickWithinProvider / callSmallModel auth gates so Settings pickers
93
- // never list dedicated providers that would only fail at dispatch time.
94
- const isCallableDedicatedAuth = (auth, providerID) => {
95
- if (providerID === 'openai') {
96
- const entry = auth?.openai;
97
- return isUsableAuthEntry(entry) && (entry.type === 'api' || entry.type === 'oauth');
98
- }
99
- if (providerID === 'anthropic') {
100
- const entry = auth?.anthropic;
101
- return entry?.type === 'api' && isUsableAuthEntry(entry);
102
- }
103
- if (providerID === 'google') {
104
- const entry = auth?.google;
105
- return entry?.type === 'api' && isUsableAuthEntry(entry);
106
- }
107
- if (providerID === 'github-copilot') {
108
- return isUsableAuthEntry(getAuthEntryForProvider(auth, 'github-copilot'));
109
- }
110
- return false;
111
- };
112
-
113
- const providerHasCatalogModels = (provider) => {
114
- if (!provider || typeof provider !== 'object') return false;
115
- return Object.values(provider.models || {}).some(
116
- (model) => typeof model?.id === 'string' && model.id.trim().length > 0,
117
- );
118
- };
119
-
120
- const shouldUseDirectAdapter = (providerID, auth) => {
121
- if (!DEDICATED_DIRECT_PROVIDERS.has(providerID)) return false;
122
- return isCallableDedicatedAuth(auth, providerID);
123
- };
124
-
125
- const addNonDedicatedCallableProvider = (ids, catalog, providerID) => {
126
- if (!providerID || providerID === 'copilot' || DEDICATED_DIRECT_PROVIDERS.has(providerID)) return;
127
- const provider = getCatalogProvider(catalog, providerID);
128
- if (providerHasCatalogModels(provider)) ids.add(providerID);
129
- };
130
-
131
- const listCallableProviderIDsForState = (auth, catalog, connectedIDs) => {
132
- const ids = new Set();
133
- for (const providerID of DEDICATED_DIRECT_PROVIDERS) {
134
- if (isCallableDedicatedAuth(auth, providerID)) ids.add(providerID);
135
- }
136
- if (isUsableAuthEntry(getAuthEntryForProvider(auth, 'github-copilot'))) {
137
- ids.add('github-copilot');
138
- }
139
-
140
- if (Array.isArray(connectedIDs)) {
141
- for (const providerID of connectedIDs) {
142
- addNonDedicatedCallableProvider(ids, catalog, providerID);
143
- }
144
- } else {
145
- // Connected catalog failed: do not pretend plugin providers are absent.
146
- for (const providerID of Object.keys(auth || {})) {
147
- if (providerID === 'copilot') continue;
148
- if (!isUsableAuthEntry(auth[providerID])) continue;
149
- addNonDedicatedCallableProvider(ids, catalog, providerID);
150
- }
151
- }
152
- return Array.from(ids);
153
- };
154
-
155
- const listCallableModelsForState = (auth, catalog, connectedIDs) => {
156
- const result = {};
157
- for (const providerID of listCallableProviderIDsForState(auth, catalog, connectedIDs)) {
158
- if (providerID === 'openai' && auth.openai?.type === 'oauth') {
159
- result[providerID] = ['gpt-5.4-mini'];
160
- continue;
161
- }
162
- if (providerID === 'github-copilot') {
163
- result[providerID] = ['gpt-5.4-nano'];
164
- continue;
165
- }
166
- const provider = getCatalogProvider(catalog, providerID);
167
- const modelIDs = Object.values(provider?.models || {})
168
- .map((model) => (typeof model?.id === 'string' ? model.id : ''))
169
- .filter(Boolean);
170
- if (modelIDs.length > 0) {
171
- result[providerID] = modelIDs;
172
- }
173
- }
174
- return result;
175
- };
176
-
177
- const resolveDefaultSummaryModel = (auth, catalog, connectedIDs) => {
178
- const callableModels = listCallableModelsForState(auth, catalog, connectedIDs);
179
- const providerIDs = Object.keys(callableModels);
180
- const providerID = callableModels.openai ? 'openai' : providerIDs[0];
181
- const modelID = providerID ? callableModels[providerID]?.[0] : undefined;
182
- return providerID && modelID
183
- ? { providerID, modelID, source: 'summary-default' }
184
- : null;
185
- };
186
-
187
83
  const readConfiguredSmallModel = (workingDirectory) => {
188
84
  try {
189
85
  const { mergedConfig } = readConfigLayers(workingDirectory);
@@ -203,39 +99,117 @@ const readConfiguredSmallModel = (workingDirectory) => {
203
99
  * buildOpenCodeUrl: (pathname: string, search?: string) => string,
204
100
  * getOpenCodeAuthHeaders: () => Record<string, string>,
205
101
  * getModelCatalog?: (directory?: string) => Promise<object>,
206
- * getConnectedCatalog?: (directory?: string) => Promise<{ connected?: string[] }>,
102
+ * openCodeClientFactory?: () => object,
103
+ * createChatCompletion?: typeof createChatCompletion,
104
+ * persistSessionMetadata?: (sessionID: string, patch: object) => Promise<unknown>,
105
+ * onSystemSessionPersisted?: (input: { sessionID: string, directory: string, metadata: object }) => void,
207
106
  * }} dependencies
208
107
  */
209
108
  export function createSmallModelService(dependencies) {
210
109
  const buildOpenCodeUrl = dependencies.buildOpenCodeUrl;
211
110
  const getOpenCodeAuthHeaders = dependencies.getOpenCodeAuthHeaders;
111
+ const persistSessionMetadata = dependencies.persistSessionMetadata || null;
112
+ const onSystemSessionPersisted = dependencies.onSystemSessionPersisted || null;
212
113
  const getModelCatalog = dependencies.getModelCatalog
213
114
  || createModelCatalogLoader({
214
115
  buildOpenCodeUrl,
215
116
  getOpenCodeAuthHeaders,
216
117
  }).getModelCatalog;
217
- const getConnectedCatalog = dependencies.getConnectedCatalog || (async (directory) => {
218
- const client = OpenCode.make({
219
- baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
220
- headers: getOpenCodeAuthHeaders(),
221
- });
222
- void directory;
223
- return loadConnectedCatalog(client);
224
- });
225
-
226
- const loadCallableContext = async (directory) => {
227
- const auth = readAuthFile();
228
- const catalog = await getModelCatalog(directory);
229
- let connectedIDs = null;
118
+ const openCodeClient = dependencies.openCodeClientFactory || (() => OpenCode.make({
119
+ baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
120
+ headers: getOpenCodeAuthHeaders(),
121
+ }));
122
+ const runChatCompletion = dependencies.createChatCompletion || createChatCompletion;
123
+
124
+ // No saved Summary AI model follows OpenCode's own default model.
125
+ const resolveSummaryProviderModel = async (summarySettings, directory) => {
126
+ if (summarySettings?.providerID && summarySettings?.modelID) {
127
+ return { providerID: summarySettings.providerID, modelID: summarySettings.modelID, source: 'summary-provider' };
128
+ }
129
+ let result;
230
130
  try {
231
- const connectedCatalog = await getConnectedCatalog(directory);
232
- connectedIDs = Array.isArray(connectedCatalog?.connected) ? connectedCatalog.connected : [];
233
- } catch {
234
- connectedIDs = null;
131
+ result = await openCodeClient().model.default(directory ? { location: { directory } } : undefined);
132
+ } catch (error) {
133
+ throw Object.assign(
134
+ new Error(`OpenCode default model is unavailable (${error?.message || error})`),
135
+ { statusCode: 502 },
136
+ );
235
137
  }
236
- return { auth, catalog, connectedIDs };
138
+ const providerID = typeof result?.data?.providerID === 'string' ? result.data.providerID : '';
139
+ const modelID = typeof result?.data?.id === 'string' ? result.data.id : '';
140
+ if (!providerID || !modelID) {
141
+ throw Object.assign(new Error('No OpenCode default model is configured'), { statusCode: 404 });
142
+ }
143
+ return { providerID, modelID, source: 'summary-default' };
237
144
  };
238
145
 
146
+ // Every OpenCode-backed call shares the Assistant LLM gateway: connected
147
+ // catalog check, then a throwaway-session `session.generate`.
148
+ const generateThroughGateway = async ({ resolved, prompt, system, directory }) => {
149
+ const catalog = await getModelCatalog(directory);
150
+ const clamped = clampPromptToModelLimit({ prompt, catalog, providerID: resolved.providerID, modelID: resolved.modelID });
151
+ const { completion } = await runChatCompletion({
152
+ body: {
153
+ providerID: resolved.providerID,
154
+ modelID: resolved.modelID,
155
+ messages: [
156
+ ...(system ? [{ role: 'system', content: system }] : []),
157
+ { role: 'user', content: clamped.prompt },
158
+ ],
159
+ },
160
+ buildOpenCodeUrl,
161
+ getOpenCodeAuthHeaders,
162
+ clientFactory: openCodeClient,
163
+ persistSessionMetadata,
164
+ onSystemSessionPersisted,
165
+ });
166
+ const text = completion?.choices?.[0]?.message?.content;
167
+ return {
168
+ text: typeof text === 'string' ? text.trim() : '',
169
+ providerID: resolved.providerID,
170
+ modelID: resolved.modelID,
171
+ source: resolved.source,
172
+ ...(clamped.truncated ? { inputTruncated: true } : {}),
173
+ };
174
+ };
175
+
176
+ // Summary AI (commit / session-title): the saved OpenCode model or OpenCode's
177
+ // default through the gateway, or the user's custom OpenAI-compatible API.
178
+ async function generateSummaryText({ prompt, system, maxOutputTokens, directory, purpose }) {
179
+ const summarySettings = readSummarySettings();
180
+ const effectiveSystem = summarySettings?.prompts?.[purpose]
181
+ || (typeof system === 'string' && system.trim() ? system.trim() : undefined);
182
+
183
+ if (summarySettings?.mode === 'custom') {
184
+ const custom = summarySettings.custom;
185
+ if (!custom) {
186
+ throw Object.assign(
187
+ new Error('Custom summary API requires a Base URL, model ID, and API token'),
188
+ { statusCode: 400 },
189
+ );
190
+ }
191
+ const clamped = clampPromptToModelLimit({ prompt, catalog: {}, providerID: 'custom', modelID: custom.modelID });
192
+ const text = await generateCustomSummaryText({
193
+ baseURL: custom.baseURL,
194
+ apiToken: custom.apiToken,
195
+ modelID: custom.modelID,
196
+ prompt: clamped.prompt,
197
+ system: effectiveSystem,
198
+ maxOutputTokens,
199
+ });
200
+ return {
201
+ text: text.trim(),
202
+ providerID: 'custom',
203
+ modelID: custom.modelID,
204
+ source: 'summary-custom',
205
+ ...(clamped.truncated ? { inputTruncated: true } : {}),
206
+ };
207
+ }
208
+
209
+ const resolved = await resolveSummaryProviderModel(summarySettings, directory);
210
+ return generateThroughGateway({ resolved, prompt, system: effectiveSystem, directory });
211
+ }
212
+
239
213
  const resolveCustomApiInput = (body = {}) => {
240
214
  const summarySettings = readSummarySettings();
241
215
  const baseURL = typeof body.baseURL === 'string' && body.baseURL.trim()
@@ -251,8 +225,8 @@ export function createSmallModelService(dependencies) {
251
225
  };
252
226
 
253
227
  /**
254
- * Generates text with the user's small model, resolved and authenticated
255
- * entirely server-side from the OpenCode config and auth store.
228
+ * Generates text with the user's small model, resolved server-side from
229
+ * OpenChamber settings, the OpenCode config, and the connected catalog.
256
230
  */
257
231
  async function generateSmallModelText({
258
232
  prompt,
@@ -269,48 +243,24 @@ export function createSmallModelService(dependencies) {
269
243
  throw Object.assign(new Error('prompt is required'), { statusCode: 400 });
270
244
  }
271
245
 
272
- const { auth, catalog, connectedIDs } = await loadCallableContext(directory);
273
- const summarySettings = SUMMARY_PURPOSES.has(purpose) ? readSummarySettings() : null;
274
- const summaryPrompt = summarySettings?.prompts?.[purpose];
275
- const defaultSummaryModel = SUMMARY_PURPOSES.has(purpose)
276
- ? resolveDefaultSummaryModel(auth, catalog, connectedIDs)
277
- : null;
246
+ if (SUMMARY_PURPOSES.has(purpose)) {
247
+ return generateSummaryText({ prompt: prompt.trim(), system, maxOutputTokens, directory, purpose });
248
+ }
278
249
 
279
250
  const explicit = parseModelRef(model);
280
- const summaryProvider = summarySettings?.mode === 'provider' && summarySettings.providerID && summarySettings.modelID
281
- ? {
282
- providerID: summarySettings.providerID,
283
- modelID: summarySettings.modelID,
284
- source: 'summary-provider',
285
- }
286
- : summarySettings?.mode !== 'custom'
287
- ? defaultSummaryModel
288
- : null;
289
- const summaryCustom = summarySettings?.mode === 'custom' ? summarySettings.custom : null;
290
- if (summarySettings?.mode === 'custom' && !summaryCustom) {
291
- throw Object.assign(
292
- new Error('Custom summary API requires a Base URL, model ID, and API token'),
293
- { statusCode: 400 },
294
- );
295
- }
296
- const resolved = summaryCustom
297
- ? { providerID: 'custom', modelID: summaryCustom.modelID, source: 'summary-custom' }
298
- : summaryProvider
299
- ? summaryProvider
300
- : explicit
301
- ? { ...explicit, source: 'request' }
302
- : resolveSmallModel({
303
- auth,
304
- catalog,
305
- settingsSmallModel: readSmallModelSettingsOverride(),
306
- configSmallModel: readConfiguredSmallModel(directory),
307
- preferredProviderID: summarySettings?.providerID || preferredProviderID,
308
- preferredModelID,
309
- });
251
+ const resolved = explicit
252
+ ? { ...explicit, source: 'request' }
253
+ : resolveSmallModel({
254
+ catalog: await getModelCatalog(directory),
255
+ settingsSmallModel: readSmallModelSettingsOverride(),
256
+ configSmallModel: readConfiguredSmallModel(directory),
257
+ preferredProviderID,
258
+ preferredModelID,
259
+ });
310
260
 
311
261
  if (!resolved) {
312
262
  throw Object.assign(
313
- new Error('No small model available — no authenticated provider has a suitable model'),
263
+ new Error('No small model available — no connected OpenCode provider has a suitable model'),
314
264
  { statusCode: 404 },
315
265
  );
316
266
  }
@@ -320,7 +270,7 @@ export function createSmallModelService(dependencies) {
320
270
  // model) is always allowed, anything else must stay on the session's
321
271
  // provider.
322
272
  if (restrictToPreferredProvider
323
- && !['settings', 'config', 'request', 'summary-provider', 'summary-default', 'summary-custom'].includes(resolved.source)
273
+ && !['settings', 'config', 'request'].includes(resolved.source)
324
274
  && resolved.providerID !== preferredProviderID) {
325
275
  throw Object.assign(
326
276
  new Error('No small model available within the session provider'),
@@ -328,71 +278,12 @@ export function createSmallModelService(dependencies) {
328
278
  );
329
279
  }
330
280
 
331
- const clamped = clampPromptToModelLimit({
281
+ return generateThroughGateway({
282
+ resolved,
332
283
  prompt: prompt.trim(),
333
- catalog,
334
- providerID: resolved.providerID,
335
- modelID: resolved.modelID,
284
+ system: typeof system === 'string' && system.trim() ? system.trim() : undefined,
285
+ directory,
336
286
  });
337
-
338
- const effectiveSystem = summaryPrompt || (typeof system === 'string' && system.trim() ? system.trim() : undefined);
339
-
340
- let text;
341
- if (summaryCustom || shouldUseDirectAdapter(resolved.providerID, auth)) {
342
- text = await callSmallModel({
343
- auth,
344
- catalog,
345
- workingDirectory: directory,
346
- providerID: resolved.providerID,
347
- modelID: resolved.modelID,
348
- prompt: clamped.prompt,
349
- system: effectiveSystem,
350
- maxOutputTokens,
351
- custom: summaryCustom,
352
- });
353
- } else {
354
- text = await generateViaOpenCodeSession({
355
- buildOpenCodeUrl,
356
- getOpenCodeAuthHeaders,
357
- providerID: resolved.providerID,
358
- modelID: resolved.modelID,
359
- prompt: clamped.prompt,
360
- system: effectiveSystem,
361
- purpose: typeof purpose === 'string' && purpose.trim() ? purpose.trim() : 'generate',
362
- directory,
363
- });
364
- }
365
-
366
- return {
367
- text: text.trim(),
368
- providerID: resolved.providerID,
369
- modelID: resolved.modelID,
370
- source: resolved.source,
371
- ...(clamped.truncated ? { inputTruncated: true } : {}),
372
- };
373
- }
374
-
375
- /**
376
- * Provider ids with a usable OpenCode login — the set the small model can
377
- * actually call. Used by the settings override picker to hide providers that
378
- * would only ever fail (e.g. opencode free models without a token).
379
- */
380
- async function listCallableProviders({ directory } = {}) {
381
- try {
382
- const { auth, catalog, connectedIDs } = await loadCallableContext(directory);
383
- return listCallableProviderIDsForState(auth, catalog, connectedIDs);
384
- } catch {
385
- return [];
386
- }
387
- }
388
-
389
- async function listCallableModels({ directory } = {}) {
390
- try {
391
- const { auth, catalog, connectedIDs } = await loadCallableContext(directory);
392
- return listCallableModelsForState(auth, catalog, connectedIDs);
393
- } catch {
394
- return {};
395
- }
396
287
  }
397
288
 
398
289
  async function testCustomApi(body) {
@@ -408,26 +299,19 @@ export function createSmallModelService(dependencies) {
408
299
  * Reports which model would be used, without calling it.
409
300
  */
410
301
  async function describeSmallModel({ directory, preferredProviderID, preferredModelID } = {}) {
411
- const auth = readAuthFile();
412
- const catalog = await getModelCatalog(directory);
413
- const resolved = resolveSmallModel({
414
- auth,
415
- catalog,
302
+ return resolveSmallModel({
303
+ catalog: await getModelCatalog(directory),
416
304
  settingsSmallModel: readSmallModelSettingsOverride(),
417
305
  configSmallModel: readConfiguredSmallModel(directory),
418
306
  preferredProviderID,
419
307
  preferredModelID,
420
308
  });
421
- return resolved;
422
309
  }
423
310
 
424
311
  return {
425
312
  generateSmallModelText,
426
- listCallableProviders,
427
- listCallableModels,
428
313
  testCustomApi,
429
314
  listCustomModels,
430
315
  describeSmallModel,
431
- stop: stopOpenCodeSessionTemp,
432
316
  };
433
317
  }
@@ -1,15 +1,11 @@
1
1
  import { getCatalogProvider } from './catalog.js';
2
2
 
3
- // Mirrors OpenCode's getSmallModel fallback chain, with OpenChamber's own
4
- // keyword + cheapest-first default when no session/family match applies:
3
+ // Mirrors OpenCode's getSmallModel fallback chain over the connected OpenCode
4
+ // catalog, with OpenChamber's own keyword + cheapest-first default when no
5
+ // session/family match applies:
5
6
  // 1. `small_model` from the merged config layers ("provider/model").
6
- // 2. Family-priority / keyword scan of authenticated providers' catalog models.
7
- // 3. GitHub Copilot's hidden utility models when Copilot is logged in.
7
+ // 2. Family-priority / keyword scan of connected providers' catalog models.
8
8
  const FAMILY_PRIORITY = ['gemini-flash', 'gpt-nano', 'claude-haiku'];
9
- const COPILOT_UTILITY_MODELS = ['gpt-5.4-nano', 'gpt-4.1', 'gpt-4o', 'gpt-4o-mini'];
10
- // The ChatGPT-plan codex backend only accepts a small allowlist of models
11
- // (nano/API-key models are rejected with 400) — this is its cheapest one.
12
- const OPENAI_OAUTH_SMALL_MODEL = 'gpt-5.4-mini';
13
9
 
14
10
  // Tokenize model ids so "mini" does not match inside "gemini".
15
11
  const modelIdTokens = (id) => String(id || '')
@@ -33,32 +29,6 @@ const SMALL_MODEL_KEYWORD_TIERS = Object.freeze([
33
29
  { tier: 8, test: (id) => idHasToken(id, 'chat') },
34
30
  ]);
35
31
 
36
- const AUTH_PROVIDER_ALIASES = {
37
- 'github-copilot': ['github-copilot', 'copilot'],
38
- };
39
-
40
- export function getAuthEntryForProvider(auth, providerID) {
41
- const aliases = AUTH_PROVIDER_ALIASES[providerID] || [providerID];
42
- for (const alias of aliases) {
43
- const entry = auth?.[alias];
44
- if (entry && typeof entry === 'object') {
45
- return entry;
46
- }
47
- }
48
- return null;
49
- }
50
-
51
- export function isUsableAuthEntry(entry) {
52
- if (!entry || typeof entry !== 'object') return false;
53
- if (entry.type === 'api') return typeof entry.key === 'string' && entry.key.length > 0;
54
- if (entry.type === 'oauth') {
55
- return (typeof entry.access === 'string' && entry.access.length > 0)
56
- || (typeof entry.refresh === 'string' && entry.refresh.length > 0);
57
- }
58
- if (entry.type === 'wellknown') return typeof entry.token === 'string' && entry.token.length > 0;
59
- return false;
60
- }
61
-
62
32
  export function parseModelRef(value) {
63
33
  if (typeof value !== 'string') return null;
64
34
  const trimmed = value.trim();
@@ -115,71 +85,25 @@ export function rankSmallModelCandidates(models) {
115
85
  .map((entry) => entry.model);
116
86
  }
117
87
 
118
- // Small-model candidates within ONE provider, by family priority. Copilot and
119
- // ChatGPT-plan OpenAI have fixed small models that never appear in the
120
- // catalog; everyone else is scanned through the catalog families.
121
- // Auth-type constraints for the four minimal hardcoded providers:
122
- // OpenAI OAuth → hardcoded codex-small only; OpenAI API key → catalog families;
123
- // Anthropic/Google require API keys; Copilot supports auth aliases.
124
- const pickWithinProvider = (providerID, auth, catalog, family) => {
125
- if (providerID === 'openai' && auth.openai?.type === 'oauth') {
126
- if (!isUsableAuthEntry(auth.openai)) return null;
127
- return family === 'gpt-nano'
128
- ? { providerID, modelID: OPENAI_OAUTH_SMALL_MODEL, source: 'codex-small' }
129
- : null;
130
- }
131
- if (providerID === 'github-copilot') {
132
- if (!isUsableAuthEntry(getAuthEntryForProvider(auth, 'github-copilot'))) return null;
133
- return family === 'gpt-nano'
134
- ? { providerID, modelID: COPILOT_UTILITY_MODELS[0], source: 'copilot-utility' }
135
- : null;
136
- }
137
- if (providerID === 'openai') {
138
- if (auth.openai?.type !== 'api' || !isUsableAuthEntry(auth.openai)) return null;
139
- }
140
- if (providerID === 'anthropic') {
141
- if (auth.anthropic?.type !== 'api' || !isUsableAuthEntry(auth.anthropic)) return null;
142
- }
143
- if (providerID === 'google') {
144
- if (auth.google?.type !== 'api' || !isUsableAuthEntry(auth.google)) return null;
145
- }
146
- const provider = getCatalogProvider(catalog, providerID);
147
- if (!provider || !provider.models || typeof provider.models !== 'object') return null;
148
- const model = pickByFamily(provider.models, family);
88
+ const catalogModels = (catalog, providerID) => {
89
+ const models = getCatalogProvider(catalog, providerID)?.models;
90
+ return models && typeof models === 'object' ? models : null;
91
+ };
92
+
93
+ const pickWithinProvider = (catalog, providerID, family) => {
94
+ const models = catalogModels(catalog, providerID);
95
+ const model = models ? pickByFamily(models, family) : null;
149
96
  return model?.id ? { providerID, modelID: model.id, source: 'family-scan' } : null;
150
97
  };
151
98
 
152
- const pickKeywordDefault = (auth, catalog, excludeProviderID) => {
99
+ const pickKeywordDefault = (catalog, providerIDs) => {
153
100
  /** @type {Array<{ providerID: string, model: object }>} */
154
101
  const candidates = [];
155
- const providerIDs = Object.keys(auth || {}).filter((providerID) => {
156
- if (providerID === 'copilot') return false;
157
- if (excludeProviderID && providerID === excludeProviderID) return false;
158
- return isUsableAuthEntry(getAuthEntryForProvider(auth, providerID));
159
- });
160
- // Surface github-copilot when only the `copilot` alias is present.
161
- if (
162
- !providerIDs.includes('github-copilot')
163
- && isUsableAuthEntry(getAuthEntryForProvider(auth, 'github-copilot'))
164
- && excludeProviderID !== 'github-copilot'
165
- ) {
166
- providerIDs.push('github-copilot');
167
- }
168
-
169
102
  for (const providerID of providerIDs) {
170
- if (providerID === 'openai' && auth.openai?.type === 'oauth') continue;
171
- if (providerID === 'github-copilot') continue;
172
- if (providerID === 'openai' && (auth.openai?.type !== 'api' || !isUsableAuthEntry(auth.openai))) continue;
173
- if (providerID === 'anthropic' && (auth.anthropic?.type !== 'api' || !isUsableAuthEntry(auth.anthropic))) continue;
174
- if (providerID === 'google' && (auth.google?.type !== 'api' || !isUsableAuthEntry(auth.google))) continue;
175
-
176
- const provider = getCatalogProvider(catalog, providerID);
177
- if (!provider?.models) continue;
178
- for (const model of rankSmallModelCandidates(provider.models)) {
103
+ for (const model of rankSmallModelCandidates(catalogModels(catalog, providerID))) {
179
104
  candidates.push({ providerID, model });
180
105
  }
181
106
  }
182
-
183
107
  if (candidates.length === 0) return null;
184
108
  candidates.sort((a, b) => {
185
109
  const tierDiff = smallModelKeywordTier(a.model) - smallModelKeywordTier(b.model);
@@ -190,7 +114,16 @@ const pickKeywordDefault = (auth, catalog, excludeProviderID) => {
190
114
  return { providerID: best.providerID, modelID: best.model.id, source: 'keyword-scan' };
191
115
  };
192
116
 
193
- export function resolveSmallModel({ auth, catalog, settingsSmallModel, configSmallModel, preferredProviderID, preferredModelID }) {
117
+ /**
118
+ * @param {{
119
+ * catalog: object | null,
120
+ * settingsSmallModel?: string | null,
121
+ * configSmallModel?: string | null,
122
+ * preferredProviderID?: string,
123
+ * preferredModelID?: string,
124
+ * }} input
125
+ */
126
+ export function resolveSmallModel({ catalog, settingsSmallModel, configSmallModel, preferredProviderID, preferredModelID }) {
194
127
  // OpenChamber's own setting (Settings → Sessions → Small Model override)
195
128
  // outranks everything, including the OpenCode config.
196
129
  const fromSettings = parseModelRef(settingsSmallModel);
@@ -210,47 +143,29 @@ export function resolveSmallModel({ auth, catalog, settingsSmallModel, configSma
210
143
  const preferred = typeof preferredProviderID === 'string' && preferredProviderID
211
144
  ? preferredProviderID
212
145
  : null;
213
- if (preferred && isUsableAuthEntry(getAuthEntryForProvider(auth, preferred))) {
146
+ if (preferred && getCatalogProvider(catalog, preferred)) {
214
147
  for (const family of FAMILY_PRIORITY) {
215
- const match = pickWithinProvider(preferred, auth, catalog, family);
148
+ const match = pickWithinProvider(catalog, preferred, family);
216
149
  if (match) return match;
217
150
  }
218
- const preferredCatalog = getCatalogProvider(catalog, preferred);
219
- if (preferredCatalog?.models) {
220
- const ranked = rankSmallModelCandidates(preferredCatalog.models);
221
- if (ranked[0]?.id) {
222
- return { providerID: preferred, modelID: ranked[0].id, source: 'keyword-scan' };
223
- }
151
+ const ranked = rankSmallModelCandidates(catalogModels(catalog, preferred));
152
+ if (ranked[0]?.id) {
153
+ return { providerID: preferred, modelID: ranked[0].id, source: 'keyword-scan' };
224
154
  }
225
155
  if (typeof preferredModelID === 'string' && preferredModelID) {
226
156
  return { providerID: preferred, modelID: preferredModelID, source: 'session-model' };
227
157
  }
228
158
  }
229
159
 
230
- // No session context (or its provider has no usable login): scan all
231
- // authenticated providers by family priority, then keyword+cost default.
232
- const authedProviders = Object.keys(auth || {}).filter((providerID) =>
233
- providerID !== preferred && isUsableAuthEntry(auth[providerID]));
234
-
160
+ // No session context (or its provider is not connected): scan the other
161
+ // connected providers by family priority, then keyword+cost default.
162
+ const otherProviders = Object.keys(catalog || {}).filter((providerID) => providerID !== preferred);
235
163
  for (const family of FAMILY_PRIORITY) {
236
- for (const providerID of authedProviders) {
237
- const match = pickWithinProvider(providerID, auth, catalog, family);
164
+ for (const providerID of otherProviders) {
165
+ const match = pickWithinProvider(catalog, providerID, family);
238
166
  if (match) return match;
239
167
  }
240
168
  }
241
169
 
242
- const keywordDefault = pickKeywordDefault(auth, catalog, preferred);
243
- if (keywordDefault) return keywordDefault;
244
-
245
- // Copilot's utility fallback for legacy auth aliases the loop above missed.
246
- const copilotEntry = getAuthEntryForProvider(auth, 'github-copilot');
247
- if (isUsableAuthEntry(copilotEntry)) {
248
- return {
249
- providerID: 'github-copilot',
250
- modelID: COPILOT_UTILITY_MODELS[0],
251
- source: 'copilot-utility',
252
- };
253
- }
254
-
255
- return null;
170
+ return pickKeywordDefault(catalog, otherProviders);
256
171
  }