@ljwei-stak/dsh-model-route-bot 0.66.4 → 0.67.0

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 (271) hide show
  1. package/.dsh-plugin/bot-product.mjs +81 -5
  2. package/.dsh-plugin/client.js +17490 -1791
  3. package/README.i18n.yaml +4 -4
  4. package/README.md +409 -364
  5. package/README.zh.md +409 -364
  6. package/apps/integration-client/README.md +31 -7
  7. package/apps/integration-client/api.mjs +14 -3
  8. package/apps/integration-client/binding-editor.jsx +308 -109
  9. package/apps/integration-client/binding-model.mjs +262 -0
  10. package/apps/integration-client/binding.css +144 -0
  11. package/apps/integration-client/bot-model-status.mjs +18 -0
  12. package/apps/integration-client/bot-profile-identity.jsx +197 -0
  13. package/apps/integration-client/bot-profile.css +57 -0
  14. package/apps/integration-client/bot-profile.mjs +97 -0
  15. package/apps/integration-client/bot-settings-dialog.css +18 -0
  16. package/apps/integration-client/bot-settings-dialog.jsx +111 -0
  17. package/apps/integration-client/bot-thread-header-bridge.css +20 -0
  18. package/apps/integration-client/bot-thread-header.jsx +278 -0
  19. package/apps/integration-client/bot-threads-panel.jsx +378 -0
  20. package/apps/integration-client/bot-threads.css +169 -0
  21. package/apps/integration-client/chat-design.mjs +250 -0
  22. package/apps/integration-client/chat-ui.jsx +175 -0
  23. package/apps/integration-client/chat.css +477 -0
  24. package/apps/integration-client/composer-draft-binding.mjs +50 -0
  25. package/apps/integration-client/composer-draft-bound.jsx +14 -0
  26. package/apps/integration-client/composer-draft-host.jsx +142 -0
  27. package/apps/integration-client/composer-draft-session.jsx +287 -0
  28. package/apps/integration-client/composer-draft-witness.mjs +115 -0
  29. package/apps/integration-client/composer-draft.css +4 -0
  30. package/apps/integration-client/composer-draft.mjs +322 -0
  31. package/apps/integration-client/contact-attention.jsx +54 -0
  32. package/apps/integration-client/data-panel.jsx +439 -0
  33. package/apps/integration-client/data.css +69 -0
  34. package/apps/integration-client/desktop-bridge.mjs +1 -0
  35. package/apps/integration-client/desktop-http-socket.mjs +49 -0
  36. package/apps/integration-client/desktop-keyboard.mjs +32 -0
  37. package/apps/integration-client/desktop-pane.jsx +99 -30
  38. package/apps/integration-client/desktop-stream.mjs +13 -3
  39. package/apps/integration-client/desktop-tools.jsx +19 -0
  40. package/apps/integration-client/desktop-touch.mjs +27 -0
  41. package/apps/integration-client/desktop.css +41 -0
  42. package/apps/integration-client/independent-chat-controller.mjs +224 -0
  43. package/apps/integration-client/independent-chat-entry.jsx +61 -0
  44. package/apps/integration-client/independent-chat-navigation.mjs +21 -0
  45. package/apps/integration-client/independent-chat-page.jsx +148 -0
  46. package/apps/integration-client/independent-chat-rich.jsx +64 -0
  47. package/apps/integration-client/independent-chat-rich.mjs +30 -0
  48. package/apps/integration-client/independent-chat.css +73 -0
  49. package/apps/integration-client/index.jsx +24 -16
  50. package/apps/integration-client/library-panel.jsx +4 -1
  51. package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
  52. package/apps/integration-client/main-route-editor.jsx +94 -0
  53. package/apps/integration-client/main-route-model.mjs +100 -0
  54. package/apps/integration-client/model-picker.css +37 -0
  55. package/apps/integration-client/model-picker.jsx +83 -0
  56. package/apps/integration-client/native-activity.css +141 -0
  57. package/apps/integration-client/native-activity.jsx +276 -0
  58. package/apps/integration-client/native-activity.mjs +384 -0
  59. package/apps/integration-client/notifications-panel.jsx +186 -0
  60. package/apps/integration-client/notifications.css +126 -0
  61. package/apps/integration-client/pairing-qr.jsx +79 -0
  62. package/apps/integration-client/panels.jsx +126 -115
  63. package/apps/integration-client/product-slots.jsx +46 -0
  64. package/apps/integration-client/routine-recorder.mjs +23 -2
  65. package/apps/integration-client/secure-pairing.jsx +84 -0
  66. package/apps/integration-client/tasks-panel.jsx +32 -10
  67. package/apps/integration-client/ui-icons.jsx +17 -0
  68. package/apps/integration-client/usage-explorer.jsx +43 -0
  69. package/apps/integration-client/usage-report.mjs +71 -0
  70. package/apps/integration-client/usage.css +34 -0
  71. package/apps/integration-client/web-panel-model.mjs +140 -0
  72. package/apps/integration-client/web-panel-page.jsx +221 -0
  73. package/apps/integration-client/web-panel-pins-live.jsx +28 -0
  74. package/apps/integration-client/web-panel.css +139 -0
  75. package/apps/integration-client/workspace-model.mjs +67 -0
  76. package/apps/integration-client/workspace-navigation.mjs +56 -0
  77. package/apps/integration-client/workspace-ui.jsx +173 -0
  78. package/apps/integration-client/workspace.css +332 -0
  79. package/modules/bot-runtime/SOURCE.json +176 -50
  80. package/modules/bot-runtime/index.mjs +169 -13
  81. package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
  82. package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
  83. package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
  84. package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
  85. package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
  86. package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
  87. package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
  88. package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
  89. package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
  90. package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
  91. package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
  92. package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
  93. package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
  94. package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
  95. package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
  96. package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
  97. package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
  98. package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
  99. package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
  100. package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
  101. package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
  102. package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
  103. package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
  104. package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
  105. package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
  106. package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
  107. package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
  108. package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
  109. package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
  110. package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
  111. package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
  112. package/modules/bot-runtime/read-model.mjs +19 -4
  113. package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
  114. package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
  115. package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
  116. package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
  117. package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
  118. package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
  119. package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
  120. package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
  121. package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
  122. package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
  123. package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
  124. package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
  125. package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
  126. package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
  127. package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
  128. package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
  129. package/modules/independent-runtime/index.mjs +152 -62
  130. package/modules/independent-runtime/remote-tools.mjs +8 -5
  131. package/modules/openmaus-protocol/NOTICE +10 -0
  132. package/modules/openmaus-protocol/README.md +15 -0
  133. package/modules/openmaus-protocol/SOURCE.json +68 -0
  134. package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
  135. package/modules/openmaus-protocol/index.mjs +1 -0
  136. package/modules/openmaus-protocol/transport.mjs +77 -0
  137. package/modules/openmaus-protocol/vendor/LICENSE +202 -0
  138. package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
  139. package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
  140. package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
  141. package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
  142. package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
  143. package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
  144. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
  145. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
  146. package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
  147. package/package.json +18 -6
  148. package/packages/bot-acp/README.md +156 -0
  149. package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
  150. package/packages/bot-acp/licenses/NOTICE.md +27 -0
  151. package/packages/bot-acp/src/acp-adapter.mjs +795 -0
  152. package/packages/bot-acp/src/acp-host.mjs +148 -0
  153. package/packages/bot-acp/src/native-launch.mjs +121 -0
  154. package/packages/bot-acp/src/permission-policy.mjs +283 -0
  155. package/packages/bot-acp/src/rpc-codec.mjs +265 -0
  156. package/packages/bot-acp/src/session-core.mjs +581 -0
  157. package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
  158. package/packages/bot-engines/README.md +281 -0
  159. package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
  160. package/packages/bot-engines/licenses/NOTICE.md +32 -0
  161. package/packages/bot-engines/src/claude-engine.mjs +772 -0
  162. package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
  163. package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
  164. package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
  165. package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
  166. package/packages/bot-engines/src/claude-protocol.mjs +177 -0
  167. package/packages/bot-engines/src/driver-catalog.mjs +334 -0
  168. package/packages/bot-engines/src/host-loop-request.mjs +95 -0
  169. package/packages/bot-engines/src/host-registrations.mjs +105 -0
  170. package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
  171. package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
  172. package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
  173. package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
  174. package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
  175. package/packages/cli-runtime/src/native-peer.ps1 +51 -3
  176. package/packages/cli-runtime/src/native-process.mjs +77 -14
  177. package/packages/model-router-plugin/package.json +1 -1
  178. package/packages/orchestration/src/binding-product.mjs +19 -0
  179. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  180. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  181. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  182. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  183. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  184. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  185. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  186. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  187. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  188. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  189. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  190. package/packages/orchestration/src/bot-loop.mjs +152 -19
  191. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  192. package/packages/orchestration/src/bot-policy.mjs +43 -4
  193. package/packages/orchestration/src/bot-profile.mjs +171 -0
  194. package/packages/orchestration/src/bot-routine.mjs +4 -4
  195. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  196. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  197. package/packages/orchestration/src/bot-serving.mjs +164 -0
  198. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  199. package/packages/orchestration/src/bot-settings.mjs +50 -11
  200. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  201. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  202. package/packages/orchestration/src/bot-threads.mjs +790 -0
  203. package/packages/orchestration/src/data-product-access.mjs +37 -0
  204. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  205. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  206. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  207. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  208. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  209. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  210. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  211. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  212. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  213. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  214. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  215. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  216. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  217. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  218. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  219. package/packages/orchestration/src/model-accounting.mjs +99 -35
  220. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  221. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  222. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  223. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  224. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  225. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  226. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  227. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  228. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  229. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  230. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  231. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  232. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  233. package/packages/orchestration/src/remote-machines.mjs +82 -14
  234. package/packages/orchestration/src/routine-execution.mjs +5 -3
  235. package/packages/orchestration/src/routine-session.mjs +19 -13
  236. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  237. package/packages/orchestration/src/task-journal.mjs +29 -0
  238. package/packages/orchestration/src/task-product.mjs +17 -0
  239. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  240. package/packages/orchestration/src/team-bridge.mjs +282 -0
  241. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  242. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  243. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  244. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  245. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  246. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  247. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  248. package/remote-computer-agent/README.md +39 -29
  249. package/remote-computer-agent/build.mjs +3 -0
  250. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  253. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  255. package/remote-computer-agent/dist/cli.mjs +1688 -168
  256. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  257. package/remote-computer-agent/dist/index.mjs +1579 -122
  258. package/remote-computer-agent/dist/index.mjs.map +4 -4
  259. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  260. package/remote-computer-agent/package-lock.json +308 -2
  261. package/remote-computer-agent/package.json +2 -1
  262. package/remote-computer-agent/pair-device.ps1 +20 -0
  263. package/remote-computer-agent/setup.ps1 +38 -16
  264. package/remote-computer-agent/src/cli.ts +24 -3
  265. package/remote-computer-agent/src/desktop-session.ts +11 -3
  266. package/remote-computer-agent/src/e2e-server.ts +81 -0
  267. package/remote-computer-agent/src/pairing-page.ts +35 -0
  268. package/remote-computer-agent/src/protocol.ts +25 -25
  269. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  270. package/remote-computer-agent/src/store.ts +20 -5
  271. package/remote-computer-agent/src/transport.ts +39 -15
@@ -0,0 +1,314 @@
1
+ // Models. Every Bot runs on one model of its own, fixed when it is created and changed
2
+ // only in its settings or by a Main Bot. The team's first Main Bot has none until the
3
+ // user picks one in its chat. The Session keeps the selection durably.
4
+ import { normalize, splitRef } from './text.js'
5
+
6
+ const CATALOG_TTL_MS = 10_000
7
+ const ROUTE_WAIT_MS = 30_000
8
+ // The DeepSeek account's sign-in grant (dsh-deepseek-account-platform).
9
+ const ACCOUNT_RECORD = 'deepseek-account-platform/default'
10
+
11
+ export function install(rt) {
12
+ const { ctx, state } = rt
13
+
14
+ let modelsSynced = null
15
+ let catalogCache = { at: 0, value: [] }
16
+ // Routes DSH serves that cannot answer a request at all — a hosted loop engine
17
+ // publishes one so its Sessions can be prompted. Once one refuses with
18
+ // HOSTED_ENGINE_ROUTE it is remembered with the models its provider served then,
19
+ // and nothing chooses it again until that provider serves different models.
20
+ const refused = new Map()
21
+ const answerable = entries => (refused.size === 0 ? entries : entries.filter(entry => !refused.has(entry.ref)))
22
+ // The models a ref's provider serves, as a string to compare; undefined when it
23
+ // cannot be listed, e.g. the provider is not registered.
24
+ async function servedModels(ref) {
25
+ try { return JSON.stringify(await ctx.llm.listModels(splitRef(ref).provider)) } catch { return undefined }
26
+ }
27
+ // An adapter update may have replaced a refused placeholder with a real route, which
28
+ // serves other models; the same models again are the same route, still refused.
29
+ async function forgetChangedRefusals() {
30
+ for (const [ref, served] of refused) {
31
+ const now = await servedModels(ref)
32
+ if (now !== undefined && now !== served) refused.delete(ref)
33
+ }
34
+ }
35
+ // A model's thinking levels, resolved once and kept for a short while: they change
36
+ // only with the provider, which also drops the catalog cache.
37
+ const reasoningCache = new Map()
38
+ const dropCatalog = () => { catalogCache = { at: 0, value: [] } }
39
+ // A key set or a sign-in may be the user's fix for a Bot's failed turn (turns.js); a
40
+ // key removed or a sign-out is not. A provider that registers or changes fixes
41
+ // nothing by itself: providers register late at every start.
42
+ ctx.on?.('llm/adapters-updated', () => { dropCatalog(); reasoningCache.clear(); return forgetChangedRefusals() })
43
+ ctx.on?.('credentials/reference-updated', async (ref) => {
44
+ dropCatalog()
45
+ if (await inPlace(credentials => credentials.describe(ref))) rt.unblockBots?.({ ref })
46
+ })
47
+ // A sign-in writes the account's record. A sign-out removes it, and so does a token
48
+ // the provider refused, while the turn that failed on it still runs. Other plugins'
49
+ // records (an OAuth token refresh) say nothing about the account.
50
+ ctx.on?.('credentials/record-updated', async (key) => {
51
+ dropCatalog()
52
+ if (String(key) !== ACCOUNT_RECORD) return
53
+ if (await inPlace(credentials => credentials.describeRecord(key))) rt.unblockBots?.({ signedIn: true })
54
+ })
55
+ ctx.on?.('settings/document-updated', dropCatalog)
56
+
57
+ // Whether a credential is there after a change. One that cannot be read counts as
58
+ // there, so a fix is not missed.
59
+ async function inPlace(describe) {
60
+ const credentials = ctx.get?.('credentials')
61
+ if (credentials === undefined) return true
62
+ try { return (await describe(credentials))?.configured !== false } catch { return true }
63
+ }
64
+
65
+ // The thinking levels a model serves: `{efforts: [{id, name, description?}],
66
+ // defaultEffort?}`, or undefined when it has none or the provider cannot say.
67
+ async function reasoningOf(ref) {
68
+ const cached = reasoningCache.get(ref)
69
+ if (cached !== undefined && Date.now() - cached.at < CATALOG_TTL_MS) return cached.value
70
+ const route = splitRef(ref)
71
+ const info = route === undefined ? undefined : await ctx.llm.resolveModelInfo?.(route.provider, route.model).catch(() => undefined)
72
+ const value = info?.reasoning === undefined ? undefined : {
73
+ efforts: (info.reasoning.efforts ?? []).map(effort => ({ id: effort.id, name: effort.name ?? effort.id, ...(effort.description ? { description: effort.description } : {}) })),
74
+ ...(info.reasoning.defaultEffort === undefined ? {} : { defaultEffort: info.reasoning.defaultEffort }),
75
+ }
76
+ reasoningCache.set(ref, { at: Date.now(), value })
77
+ return value
78
+ }
79
+
80
+ // A provider's model list can change in DSH settings without any event, so a picker
81
+ // that is about to be used asks for a `fresh` one. Entries for models a Bot uses
82
+ // also carry their `reasoning`, so the picker can offer thinking levels.
83
+ async function catalog({ fresh = false } = {}) {
84
+ if (!fresh && Date.now() - catalogCache.at < CATALOG_TTL_MS && catalogCache.value.length > 0) return answerable(catalogCache.value)
85
+ const entries = []
86
+ for (const provider of ctx.llm.listProviders()) {
87
+ let models = []
88
+ try { models = await ctx.llm.listModels(provider.id) } catch { continue }
89
+ for (const model of models) {
90
+ entries.push({
91
+ ref: `${provider.id}/${model.id}`,
92
+ provider: provider.id,
93
+ providerName: provider.name ?? provider.id,
94
+ id: model.id,
95
+ name: model.name ?? model.id,
96
+ ...(model.inputModalities ? { input: [...model.inputModalities] } : {}),
97
+ })
98
+ }
99
+ }
100
+ // DSH serves a provider's models whether or not its key is set; without one, every
101
+ // turn on them fails.
102
+ const missing = await rt.missingKeys?.([...new Set(entries.map(entry => entry.provider))]) ?? new Set()
103
+ for (const entry of entries) if (missing.has(entry.provider)) entry.needsKey = true
104
+ // A key can arrive without an event (an environment variable at the next start), so
105
+ // a provider listed with its key again lifts what its missing key blocked.
106
+ rt.unblockBots?.({ keyed: new Set(entries.filter(entry => !entry.needsKey).map(entry => entry.provider)) })
107
+ const used = new Set(Object.values(state.bots ?? {}).flatMap(bot => [bot.model, bot.appliedModel].filter(Boolean)))
108
+ await Promise.all(entries.filter(entry => used.has(entry.ref)).map(async (entry) => {
109
+ const reasoning = await reasoningOf(entry.ref)
110
+ if (reasoning !== undefined) entry.reasoning = reasoning
111
+ }))
112
+ catalogCache = { at: Date.now(), value: entries }
113
+ return answerable(entries)
114
+ }
115
+
116
+ // The profile default model of DSH as set, served or not, and able to answer.
117
+ const defaultRef = () => {
118
+ const selection = ctx.get?.('agentDefaultModel')?.currentSelection?.()
119
+ if (!selection?.provider || !selection?.model) return undefined
120
+ const ref = `${selection.provider}/${selection.model}`
121
+ return refused.has(ref) ? undefined : ref
122
+ }
123
+ // A model whose provider has no key fails every turn, so no Bot is given one unasked.
124
+ const usableOf = entries => entries.filter(entry => entry.needsKey !== true)
125
+ // By the catalog as last listed, without waiting for a fresh one.
126
+ const isUsable = ref => usableOf(catalogCache.value).some(entry => entry.ref === ref)
127
+ // The profile default model of DSH, when it is served and usable; else the first
128
+ // usable model; else none.
129
+ function pickDefault(entries) {
130
+ const usable = usableOf(entries)
131
+ const ref = defaultRef()
132
+ // Neither a refused route nor one that fails for a missing key is picked.
133
+ const list = answerable(usable)
134
+ return list.find(entry => entry.ref === ref)?.ref ?? list[0]?.ref
135
+ }
136
+ const defaultModel = async () => pickDefault(await catalog())
137
+
138
+ // The model a create that names none starts on: the team's pick while the catalog
139
+ // still serves it, else the DSH default. A create fixes the ref then, so a later
140
+ // change of either default leaves the Bot alone.
141
+ const defaultBotModel = async () => {
142
+ const entries = await catalog()
143
+ const preferred = state.prefs.defaultBotModel
144
+ return entries.some(entry => entry.ref === preferred) ? preferred : pickDefault(entries)
145
+ }
146
+
147
+ // A full `provider/model` ref, or a model id or name that only one provider serves.
148
+ async function resolveModel(text) {
149
+ const entries = await catalog({ fresh: true })
150
+ const wanted = String(text ?? '').trim()
151
+ const exact = entries.find(entry => entry.ref === wanted)
152
+ if (exact) return exact.ref
153
+ const key = normalize(wanted)
154
+ const named = entries.filter(entry => normalize(entry.id) === key || normalize(entry.name) === key)
155
+ if (named.length === 1) return named[0].ref
156
+ const known = entries.map(entry => entry.ref).join(', ') || 'none'
157
+ throw new Error(named.length > 1
158
+ ? `Model ${wanted} is served by more than one provider; use one of ${named.map(entry => entry.ref).join(', ')}`
159
+ : `Model ${wanted} is not available. Available models: ${known}`)
160
+ }
161
+
162
+ // A Bot with no model yet waits for the user to pick one and changes nothing. A
163
+ // model that is no longer served falls back to the last one it ran on, then the
164
+ // default; a fallback is never one that needs a key.
165
+ async function desiredModel(bot) {
166
+ if (!bot.model) return undefined
167
+ const entries = await catalog()
168
+ if (entries.some(entry => entry.ref === bot.model)) return bot.model
169
+ return usableOf(entries).some(entry => entry.ref === bot.appliedModel) ? bot.appliedModel : pickDefault(entries)
170
+ }
171
+
172
+ // The thinking level a Bot asked for, when the model it runs on serves it; else the
173
+ // model's own default applies. A level the model does not have is ignored, never
174
+ // sent: an explicit effort a provider does not know is a hard refusal there.
175
+ async function desiredEffort(bot, ref) {
176
+ if (bot.effort === undefined || ref === undefined) return undefined
177
+ const reasoning = await reasoningOf(ref)
178
+ return reasoning?.efforts?.some(effort => effort.id === bot.effort) ? bot.effort : undefined
179
+ }
180
+
181
+ // A thinking level id, or a name only one level of the model has, like resolveModel.
182
+ async function resolveEffort(ref, text) {
183
+ const wanted = String(text ?? '').trim()
184
+ if (wanted === '') return undefined
185
+ const reasoning = await reasoningOf(ref)
186
+ const efforts = reasoning?.efforts ?? []
187
+ if (efforts.length === 0) throw new Error(`Model ${ref} has no thinking levels`)
188
+ const key = normalize(wanted)
189
+ const named = efforts.filter(effort => normalize(effort.id) === key || normalize(effort.name) === key)
190
+ if (named.length === 1) return named[0].id
191
+ throw new Error(`Thinking level ${wanted} is not available for ${ref}. Available: ${efforts.map(effort => effort.name).join(', ')}`)
192
+ }
193
+
194
+ // The profile default model's thinking level, when the model a Bot starts on
195
+ // serves it. A level the model does not know is ignored, like an inherited
196
+ // `bot.effort` — the model's own default applies instead.
197
+ async function defaultEffort(ref) {
198
+ const wanted = ctx.get?.('agentDefaultModel')?.currentSelection?.()?.reasoningEffort
199
+ if (ref === undefined || wanted === undefined) return undefined
200
+ return resolveEffort(ref, wanted).catch(() => undefined)
201
+ }
202
+
203
+ // DSH's selectModel also saves the selection as the profile default model, in the
204
+ // background. A Bot's model is its own, so the user's default is written back after
205
+ // it. Selections run one at a time and wait for the write-back: profile writes land in
206
+ // submission order, so the next one reads the user's default, not a Bot's.
207
+ let selecting = Promise.resolve()
208
+ // Recheck group Sessions after every restart: Bot-level applied fields cannot
209
+ // prove that an older group Session received the member's thinking level.
210
+ const sessionSelections = new Map()
211
+ function selectSessionModel(sessionId, { provider, model, reasoningEffort }) {
212
+ const run = selecting.then(async () => {
213
+ const defaults = ctx.get?.('agentDefaultModel')
214
+ const before = defaults?.currentSelection?.()
215
+ const result = await ctx.sessionController.selectModel({
216
+ sessionId, provider, model,
217
+ ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
218
+ })
219
+ const saved = result?.selected ?? { provider, model, ...(reasoningEffort === undefined ? {} : { reasoningEffort }) }
220
+ if (before?.provider && before?.model && !sameSelection(before, saved)) {
221
+ await defaults.saveSelection(before).catch(error => rt.warn('ds-bot: the default model was not restored: %s', error))
222
+ }
223
+ sessionSelections.set(sessionId, { provider, model, reasoningEffort })
224
+ return result
225
+ })
226
+ selecting = run.catch(() => {})
227
+ return run
228
+ }
229
+
230
+ const sameSelection = (a, b) => a.provider === b.provider && a.model === b.model && a.reasoningEffort === b.reasoningEffort
231
+
232
+ // `force` re-selects even when the record says it is applied: the composer's own
233
+ // model picker can move the Session without telling this plugin.
234
+ async function applyModel(bot, { force = false } = {}) {
235
+ const ref = await desiredModel(bot)
236
+ if (ref === undefined) return ref
237
+ const effort = await desiredEffort(bot, ref)
238
+ const ownChanged = force || bot.appliedModel !== ref || (bot.appliedEffort ?? undefined) !== effort
239
+ const route = { ...splitRef(ref), ...(effort === undefined ? {} : { reasoningEffort: effort }) }
240
+ if (ownChanged) await selectSessionModel(rt.chatOf(bot.id), route)
241
+ for (const sessionId of rt.groupSessionsOf(bot.id)) {
242
+ const applied = sessionSelections.get(sessionId)
243
+ if (!force && applied !== undefined && sameSelection(applied, route)) continue
244
+ await selectSessionModel(sessionId, route)
245
+ .catch(error => rt.warn('ds-bot: model for %s in a group not applied: %s', bot.name, error))
246
+ }
247
+ if (!ownChanged) return ref
248
+ bot.appliedModel = ref
249
+ if (effort === undefined) delete bot.appliedEffort
250
+ else bot.appliedEffort = effort
251
+ await rt.save()
252
+ return ref
253
+ }
254
+
255
+ // A provider may register after the plugin starts; a greeting waits for its Bot's
256
+ // model instead of failing with "no adapter registered".
257
+ async function waitForModel(bot, ms = ROUTE_WAIT_MS) {
258
+ const deadline = Date.now() + ms
259
+ for (;;) {
260
+ if (!bot.model) return undefined
261
+ catalogCache = { at: 0, value: [] }
262
+ const served = new Set((await catalog()).map(entry => entry.ref))
263
+ if (served.has(bot.model) || Date.now() >= deadline) return desiredModel(bot)
264
+ await new Promise((resolve) => {
265
+ const timer = setTimeout(done, Math.min(2000, deadline - Date.now()))
266
+ const off = ctx.on?.('llm/adapters-updated', done)
267
+ function done() { clearTimeout(timer); off?.(); resolve() }
268
+ })
269
+ }
270
+ }
271
+
272
+ // Sequential, and again whenever the answerable models change (a refused route is
273
+ // not one of them): a provider that registers late still gets its Bots' models selected.
274
+ let syncedKey
275
+ const syncModels = () => {
276
+ const key = answerable(catalogCache.value).map(entry => entry.ref).join(',')
277
+ if (key !== syncedKey) { syncedKey = key; modelsSynced = modelsSynced?.then(syncAll, syncAll) ?? syncAll() }
278
+ return modelsSynced
279
+ }
280
+ async function syncAll() {
281
+ for (const bot of Object.values(state.bots)) {
282
+ await applyModel(bot).catch(error => rt.warn('ds-bot: model for %s not applied: %s', bot.name, error))
283
+ }
284
+ }
285
+
286
+ // A route that refused a request for not being a model endpoint: it stays out of
287
+ // the catalog from now on, the Bots pinned to it move to what they would be given
288
+ // today, and each of them re-selects even when its record looks applied (`force`):
289
+ // its Session may still be on the route, or on another Bot's route in a group.
290
+ async function routeRefused(ref, bot) {
291
+ const served = await servedModels(ref)
292
+ if (!refused.has(ref)) {
293
+ refused.set(ref, served)
294
+ rt.warn('ds-bot: %s is not a model endpoint, so it is not chosen again', ref)
295
+ const entries = await catalog()
296
+ const pinned = Object.values(state.bots).filter(other => other.model === ref || other.appliedModel === ref)
297
+ for (const other of pinned) {
298
+ if (other.model === ref) other.model = pickDefault(entries)
299
+ if (other.appliedModel === ref) delete other.appliedModel
300
+ }
301
+ await rt.save()
302
+ for (const other of pinned) {
303
+ if (other !== bot) await applyModel(other, { force: true }).catch(error => rt.warn('ds-bot: model for %s not applied: %s', other.name, error))
304
+ }
305
+ }
306
+ if (bot !== undefined) await applyModel(bot, { force: true })
307
+ }
308
+
309
+ Object.assign(rt, {
310
+ catalog, cachedCatalog: () => answerable(catalogCache.value), cachedDefault: () => pickDefault(catalogCache.value),
311
+ defaultRef, defaultModel, defaultBotModel, defaultEffort, isUsable, resolveModel, desiredModel, reasoningOf, resolveEffort, desiredEffort,
312
+ selectSessionModel, applyModel, waitForModel, syncModels, routeRefused, dropCatalog,
313
+ })
314
+ }