@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.
- package/README.md +13 -7
- package/bin/cli-opencode.test.js +208 -0
- package/bin/cli.js +62 -9
- package/bin/lib/cli-startup.js +105 -2
- package/bin/lib/cli-startup.test.js +179 -0
- package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
- package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
- package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
- package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
- package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
- package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
- package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
- package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
- package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
- package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
- package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
- package/dist/assets/SettingsView-BRxELjow.js +25 -0
- package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
- package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
- package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
- package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
- package/dist/assets/collapsible-DXbDynN4.js +3 -0
- package/dist/assets/context-menu-CqolsLCm.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
- package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
- package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-CM42qUed.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
- package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
- package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/main-BILgBI3B.js +169 -0
- package/dist/assets/main-BeveiW6F.js +2 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
- package/dist/assets/miniChat-BgrzM3u7.js +2 -0
- package/dist/assets/mobile-DtYoQE8C.js +2 -0
- package/dist/assets/multirun-jy8dEI96.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-BQghHRfp.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
- package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
- package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
- package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
- package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
- package/dist/assets/src-CLN7Z-zx.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
- package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
- package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
- package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
- package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
- package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +2 -0
- package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
- package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.js} +2 -2
- package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
- package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
- package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
- package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
- package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
- package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
- package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
- package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
- package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
- package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
- package/dist/index.html +13 -13
- package/dist/mini-chat.html +13 -13
- package/dist/mobile.html +13 -13
- package/package.json +2 -2
- package/server/index.js +392 -20
- package/server/lib/assistants/DOCUMENTATION.md +26 -21
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +454 -0
- package/server/lib/assistants/routes.js +11 -5
- package/server/lib/assistants/service.js +850 -294
- package/server/lib/assistants/service.test.js +588 -274
- package/server/lib/conversations/DOCUMENTATION.md +5 -5
- package/server/lib/conversations/service.js +87 -57
- package/server/lib/conversations/service.test.js +132 -100
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +51 -5
- package/server/lib/event-stream/runtime.js +57 -20
- package/server/lib/event-stream/runtime.test.js +112 -0
- package/server/lib/llm/DOCUMENTATION.md +95 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +41 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +516 -453
- package/server/lib/llm/generate.test.js +705 -484
- package/server/lib/llm/routes.js +8 -4
- package/server/lib/message-queue/DOCUMENTATION.md +5 -5
- package/server/lib/message-queue/opencode-adapter.js +191 -104
- package/server/lib/message-queue/opencode-adapter.test.js +328 -58
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/message-queue/runtime.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/notifications/runtime.js +5 -0
- package/server/lib/opencode/DOCUMENTATION.md +103 -16
- package/server/lib/opencode/agent-document.d.ts +28 -0
- package/server/lib/opencode/agent-document.js +280 -0
- package/server/lib/opencode/agent-document.test.js +137 -0
- package/server/lib/opencode/agents.js +96 -0
- package/server/lib/opencode/auth-state-runtime.js +5 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/config-entity-routes.js +111 -102
- package/server/lib/opencode/config-entity-routes.test.js +123 -10
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +238 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +208 -111
- package/server/lib/opencode/env-runtime.test.js +211 -14
- package/server/lib/opencode/feature-routes-runtime.js +80 -5
- package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
- package/server/lib/opencode/http-server-connections.js +98 -0
- package/server/lib/opencode/http-server-connections.test.js +321 -0
- package/server/lib/opencode/lifecycle.js +539 -95
- package/server/lib/opencode/lifecycle.test.js +698 -6
- package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
- package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
- package/server/lib/opencode/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +88 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
- package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
- package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
- package/server/lib/opencode/plugin-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- package/server/lib/opencode/provider-catalog.js +58 -12
- package/server/lib/opencode/provider-catalog.test.js +48 -0
- package/server/lib/opencode/proxy.js +368 -28
- package/server/lib/opencode/proxy.test.js +338 -0
- package/server/lib/opencode/routes.js +303 -76
- package/server/lib/opencode/routes.test.js +315 -50
- package/server/lib/opencode/runtime-contract.js +401 -0
- package/server/lib/opencode/runtime-contract.test.js +214 -0
- package/server/lib/opencode/server-opencode-fetch.js +242 -0
- package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
- package/server/lib/opencode/server-utils-runtime.js +35 -0
- package/server/lib/opencode/shared-service.integration.test.js +66 -0
- package/server/lib/opencode/shared-service.js +147 -0
- package/server/lib/opencode/shared-service.test.js +93 -0
- package/server/lib/opencode/shared.js +3 -3
- package/server/lib/opencode/shutdown-runtime.js +15 -6
- package/server/lib/opencode/shutdown-runtime.test.js +49 -1
- package/server/lib/opencode/skill-routes.js +26 -40
- package/server/lib/opencode/skill-routes.test.js +71 -7
- package/server/lib/opencode/skills.test.js +15 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
- package/server/lib/opencode/static-routes-runtime.js +3 -1
- package/server/lib/opencode/static-routes-runtime.test.js +33 -0
- package/server/lib/opencode/v1-migration-gate.js +157 -0
- package/server/lib/opencode/v1-migration-gate.test.js +111 -0
- package/server/lib/opencode/v2-client.js +60 -0
- package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
- package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
- package/server/lib/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
- package/server/lib/permission-auto-accept/runtime.js +14 -7
- package/server/lib/permission-auto-accept/runtime.test.js +27 -12
- package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
- package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
- package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
- package/server/lib/scheduled-tasks/routes.js +27 -0
- package/server/lib/scheduled-tasks/routes.test.js +94 -0
- package/server/lib/scheduled-tasks/runtime.js +381 -310
- package/server/lib/scheduled-tasks/runtime.test.js +600 -309
- package/server/lib/session-goal/DOCUMENTATION.md +173 -22
- package/server/lib/session-goal/capability.js +57 -0
- package/server/lib/session-goal/capability.test.js +33 -0
- package/server/lib/session-goal/routes.js +240 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +1617 -113
- package/server/lib/session-goal/runtime.test.js +1500 -70
- package/server/lib/session-index/DOCUMENTATION.md +18 -15
- package/server/lib/session-index/event-ingest.js +89 -16
- package/server/lib/session-index/event-ingest.test.js +73 -0
- package/server/lib/session-index/host-authority-snapshot.js +92 -0
- package/server/lib/session-index/routes.host-archive.test.js +201 -0
- package/server/lib/session-index/routes.js +63 -10
- package/server/lib/session-index/routes.test.js +8 -8
- package/server/lib/session-index/service.js +7 -1
- package/server/lib/session-index/sync-runtime.js +171 -41
- package/server/lib/session-index/sync-runtime.test.js +164 -9
- package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
- package/server/lib/session-metadata/routes.js +193 -0
- package/server/lib/session-metadata/routes.test.js +156 -0
- package/server/lib/session-metadata/session-archive.d.ts +73 -0
- package/server/lib/session-metadata/session-archive.js +654 -0
- package/server/lib/session-metadata/session-archive.test.js +359 -0
- package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
- package/server/lib/session-metadata/session-metadata-store.js +375 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
- package/server/lib/session-metadata/session-projection.d.ts +41 -0
- package/server/lib/session-metadata/session-projection.js +255 -0
- package/server/lib/session-metadata/session-projection.test.js +64 -0
- package/server/lib/session-metadata/system-session.js +55 -0
- package/server/lib/session-metadata/system-session.test.js +55 -0
- package/server/lib/session-title/DOCUMENTATION.md +40 -7
- package/server/lib/session-title/native-runtime.test.js +167 -0
- package/server/lib/session-title/runtime.js +67 -42
- package/server/lib/session-title/session-access.js +46 -0
- package/server/lib/session-title/session-access.test.js +91 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
- package/server/lib/session-turn-pages/reconcile.service.js +1 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
- package/server/lib/session-turn-pages/routes.js +123 -75
- package/server/lib/session-turn-pages/routes.test.js +566 -0
- package/server/lib/session-turn-pages/service.js +6 -2
- package/server/lib/session-turn-pages/service.test.js +34 -0
- package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
- package/server/lib/session-turn-pages/session-message-projection.js +281 -0
- package/server/lib/small-model/DOCUMENTATION.md +101 -146
- package/server/lib/small-model/catalog.js +84 -54
- package/server/lib/small-model/catalog.test.js +111 -9
- package/server/lib/small-model/custom-api.js +68 -0
- package/server/lib/small-model/custom-api.test.js +64 -0
- package/server/lib/small-model/index.js +125 -241
- package/server/lib/small-model/resolve.js +35 -120
- package/server/lib/small-model/resolve.test.js +20 -110
- package/server/lib/small-model/routes.js +1 -3
- package/server/lib/small-model/summary-settings.test.js +157 -253
- package/server/opencode-proxy.test.js +63 -12
- package/dist/assets/AssistantView-BUveAgc4.js +0 -3
- package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
- package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
- package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
- package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
- package/dist/assets/SettingsView-PkXIavr7.js +0 -25
- package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
- package/dist/assets/collapsible-D5Phaykh.js +0 -1
- package/dist/assets/context-menu-CubSDW2g.js +0 -1
- package/dist/assets/es-DA3MqOo0.js +0 -15
- package/dist/assets/fr-Dbz0CdIA.js +0 -13
- package/dist/assets/gitApi-DXyC4MiV.js +0 -7
- package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
- package/dist/assets/ja-DumanVQA.js +0 -15
- package/dist/assets/ko-C0IkiUjQ.js +0 -15
- package/dist/assets/main-BPS5ylib.js +0 -2
- package/dist/assets/main-Cpd9VWYs.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-B8_KLWiY.js +0 -2
- package/dist/assets/mobile-CtktKrXn.js +0 -2
- package/dist/assets/multirun-CYfP9Cfm.js +0 -4
- package/dist/assets/pl-B9No6MQp.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
- package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
- package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/store-5B_LDnLM.js +0 -15
- package/dist/assets/uk-C_1KkqyP.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
- package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
- package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
- package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-uWILgBdt.js +0 -15
- package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
- package/server/lib/small-model/call.js +0 -427
- package/server/lib/small-model/call.test.js +0 -382
- package/server/lib/small-model/opencode-session.js +0 -339
- package/server/lib/small-model/opencode-session.test.js +0 -177
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
MESSAGE_STREAM_GLOBAL_WS_PATH,
|
|
7
7
|
MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS,
|
|
8
8
|
sendMessageStreamWsEvent,
|
|
9
|
+
sendMessageStreamWsFrame,
|
|
9
10
|
} from './protocol.js';
|
|
10
11
|
import { createGlobalMessageStreamHub } from './global-hub.js';
|
|
11
12
|
import { createGlobalMessageStreamWsBridge } from './global-ws-bridge.js';
|
|
@@ -62,6 +63,9 @@ export function createMessageStreamWsRuntime({
|
|
|
62
63
|
buildOpenCodeUrl,
|
|
63
64
|
getOpenCodeAuthHeaders,
|
|
64
65
|
processForwardedEventPayload,
|
|
66
|
+
projectOutboundSessionPayload = null,
|
|
67
|
+
/** Await Host metadata ready before directory WS attaches upstream. */
|
|
68
|
+
ensureSessionMetadataReady = null,
|
|
65
69
|
wsClients,
|
|
66
70
|
triggerHealthCheck,
|
|
67
71
|
heartbeatIntervalMs = MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS,
|
|
@@ -104,29 +108,62 @@ export function createMessageStreamWsRuntime({
|
|
|
104
108
|
const includeReasoning = shouldIncludeReasoning(requestUrl.searchParams.get('includeReasoning'));
|
|
105
109
|
const reasoningFilter = includeReasoning ? null : createReasoningOutboundFilter();
|
|
106
110
|
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
// Host metadata readiness gates *both* global and directory streams before
|
|
112
|
+
// accept/hub.start — otherwise globalBridge.accept → hub.start bypasses the
|
|
113
|
+
// gate and lifecycle frames can fan out while the store is still unknown.
|
|
114
|
+
void (async () => {
|
|
115
|
+
if (typeof ensureSessionMetadataReady === 'function') {
|
|
116
|
+
try {
|
|
117
|
+
const ready = await ensureSessionMetadataReady();
|
|
118
|
+
if (!ready) {
|
|
119
|
+
sendMessageStreamWsFrame(socket, {
|
|
120
|
+
type: 'error',
|
|
121
|
+
message: 'session metadata is unavailable',
|
|
122
|
+
code: 'session_metadata_unavailable',
|
|
123
|
+
retryable: true,
|
|
124
|
+
});
|
|
125
|
+
socket.close(1013, 'session metadata unavailable');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
sendMessageStreamWsFrame(socket, {
|
|
130
|
+
type: 'error',
|
|
131
|
+
message: 'session metadata is unavailable',
|
|
132
|
+
code: 'session_metadata_unavailable',
|
|
133
|
+
retryable: true,
|
|
134
|
+
});
|
|
135
|
+
socket.close(1013, 'session metadata unavailable');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (socket.readyState !== 1) return;
|
|
141
|
+
|
|
142
|
+
if (isGlobalStream) {
|
|
143
|
+
globalBridge.accept(socket, {
|
|
144
|
+
requestedLastEventId,
|
|
145
|
+
reasoningFilter,
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
acceptDirectoryMessageStreamWsConnection({
|
|
151
|
+
socket,
|
|
109
152
|
requestedLastEventId,
|
|
153
|
+
requestedDirectory,
|
|
110
154
|
reasoningFilter,
|
|
155
|
+
buildOpenCodeUrl,
|
|
156
|
+
getOpenCodeAuthHeaders,
|
|
157
|
+
processForwardedEventPayload,
|
|
158
|
+
projectOutboundSessionPayload,
|
|
159
|
+
wsClients,
|
|
160
|
+
triggerHealthCheck,
|
|
161
|
+
heartbeatIntervalMs,
|
|
162
|
+
upstreamStallTimeoutMs,
|
|
163
|
+
upstreamReconnectDelayMs,
|
|
164
|
+
fetchImpl,
|
|
111
165
|
});
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
acceptDirectoryMessageStreamWsConnection({
|
|
116
|
-
socket,
|
|
117
|
-
requestedLastEventId,
|
|
118
|
-
requestedDirectory,
|
|
119
|
-
reasoningFilter,
|
|
120
|
-
buildOpenCodeUrl,
|
|
121
|
-
getOpenCodeAuthHeaders,
|
|
122
|
-
processForwardedEventPayload,
|
|
123
|
-
wsClients,
|
|
124
|
-
triggerHealthCheck,
|
|
125
|
-
heartbeatIntervalMs,
|
|
126
|
-
upstreamStallTimeoutMs,
|
|
127
|
-
upstreamReconnectDelayMs,
|
|
128
|
-
fetchImpl,
|
|
129
|
-
});
|
|
166
|
+
})();
|
|
130
167
|
});
|
|
131
168
|
|
|
132
169
|
const upgradeHandler = (req, socket, head) => {
|
|
@@ -126,6 +126,118 @@ describe('event stream broadcaster', () => {
|
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
describe('message stream websocket runtime Host metadata readiness', () => {
|
|
130
|
+
it('does not accept/start global hub while metadata load is pending', async () => {
|
|
131
|
+
const server = new EventEmitter();
|
|
132
|
+
const wsClients = new Set();
|
|
133
|
+
let fetchCalls = 0;
|
|
134
|
+
let releaseReady;
|
|
135
|
+
const readyGate = new Promise((resolve) => { releaseReady = resolve; });
|
|
136
|
+
|
|
137
|
+
const runtime = createMessageStreamWsRuntime({
|
|
138
|
+
server,
|
|
139
|
+
uiAuthController: null,
|
|
140
|
+
isRequestOriginAllowed: async () => true,
|
|
141
|
+
rejectWebSocketUpgrade() {},
|
|
142
|
+
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
|
|
143
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
144
|
+
processForwardedEventPayload() {},
|
|
145
|
+
ensureSessionMetadataReady: async () => {
|
|
146
|
+
await readyGate;
|
|
147
|
+
return true;
|
|
148
|
+
},
|
|
149
|
+
wsClients,
|
|
150
|
+
upstreamReconnectDelayMs: 0,
|
|
151
|
+
fetchImpl: async (_url, options) => {
|
|
152
|
+
fetchCalls += 1;
|
|
153
|
+
return createSseResponse({
|
|
154
|
+
signal: options.signal,
|
|
155
|
+
holdOpen: true,
|
|
156
|
+
blocks: [
|
|
157
|
+
'id: evt-1\ndata: {"type":"session.updated","properties":{"info":{"id":"ses_1","time":{}}}}\n\n',
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const socket = new FakeSocket();
|
|
164
|
+
runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
166
|
+
|
|
167
|
+
// Still pending → no hub.start → no upstream fetch, no accept frames yet.
|
|
168
|
+
expect(fetchCalls).toBe(0);
|
|
169
|
+
expect(socket.closeCalls).toEqual([]);
|
|
170
|
+
expect(socket.sent.some((f) => f.type === 'event')).toBe(false);
|
|
171
|
+
|
|
172
|
+
releaseReady(true);
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
174
|
+
|
|
175
|
+
expect(fetchCalls).toBe(1);
|
|
176
|
+
expect(socket.sent.some((f) => f.type === 'event'
|
|
177
|
+
&& f.payload?.type === 'session.updated')).toBe(true);
|
|
178
|
+
|
|
179
|
+
socket.close();
|
|
180
|
+
await runtime.close();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('rejects global connection when metadata ready fails', async () => {
|
|
184
|
+
const server = new EventEmitter();
|
|
185
|
+
const runtime = createMessageStreamWsRuntime({
|
|
186
|
+
server,
|
|
187
|
+
uiAuthController: null,
|
|
188
|
+
isRequestOriginAllowed: async () => true,
|
|
189
|
+
rejectWebSocketUpgrade() {},
|
|
190
|
+
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
|
|
191
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
192
|
+
processForwardedEventPayload() {},
|
|
193
|
+
ensureSessionMetadataReady: async () => false,
|
|
194
|
+
wsClients: new Set(),
|
|
195
|
+
upstreamReconnectDelayMs: 0,
|
|
196
|
+
fetchImpl: async () => {
|
|
197
|
+
throw new Error('upstream must not start');
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const socket = new FakeSocket();
|
|
202
|
+
runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
204
|
+
|
|
205
|
+
expect(socket.sent.some((f) => f.type === 'error'
|
|
206
|
+
&& f.code === 'session_metadata_unavailable'
|
|
207
|
+
&& f.retryable === true)).toBe(true);
|
|
208
|
+
expect(socket.closeCalls.some((c) => c.code === 1013)).toBe(true);
|
|
209
|
+
|
|
210
|
+
await runtime.close();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('gates directory stream the same way as global', async () => {
|
|
214
|
+
const server = new EventEmitter();
|
|
215
|
+
const runtime = createMessageStreamWsRuntime({
|
|
216
|
+
server,
|
|
217
|
+
uiAuthController: null,
|
|
218
|
+
isRequestOriginAllowed: async () => true,
|
|
219
|
+
rejectWebSocketUpgrade() {},
|
|
220
|
+
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
|
|
221
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
222
|
+
processForwardedEventPayload() {},
|
|
223
|
+
ensureSessionMetadataReady: async () => false,
|
|
224
|
+
wsClients: new Set(),
|
|
225
|
+
upstreamReconnectDelayMs: 0,
|
|
226
|
+
fetchImpl: async () => {
|
|
227
|
+
throw new Error('upstream must not start');
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const socket = new FakeSocket();
|
|
232
|
+
runtime.wsServer.emit('connection', socket, { url: '/api/event/ws?directory=/repo' });
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
234
|
+
|
|
235
|
+
expect(socket.sent.some((f) => f.code === 'session_metadata_unavailable')).toBe(true);
|
|
236
|
+
expect(socket.closeCalls.some((c) => c.code === 1013)).toBe(true);
|
|
237
|
+
await runtime.close();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
129
241
|
describe('message stream websocket runtime', () => {
|
|
130
242
|
it('filters reasoning events per connection when includeReasoning=false', async () => {
|
|
131
243
|
const server = new EventEmitter();
|
|
@@ -5,103 +5,104 @@ only model path the Assistant harness (`pi-agent-core` `streamFn`) talks to.
|
|
|
5
5
|
|
|
6
6
|
## Contract
|
|
7
7
|
|
|
8
|
-
- `GET /api/openchamber/llm/models` —
|
|
9
|
-
from
|
|
10
|
-
|
|
8
|
+
- `GET /api/openchamber/llm/models` — available `{providerID, modelID}` catalog
|
|
9
|
+
from official `@opencode/client` `provider.list` + `model.list`
|
|
10
|
+
(`{ location, data }`). `ModelInfo.id` is the external modelID used in
|
|
11
|
+
generate/session refs (`ModelInfo.modelID` is a separate internal field).
|
|
12
|
+
Vision prefers `capabilities.input.image === true` (SDK object shape), then
|
|
13
|
+
v2 list `capabilities.input` containing `"image"`, then legacy
|
|
14
|
+
modalities/input/attachment. Catalog loads use a bounded AbortSignal
|
|
15
|
+
(default 8s) so a pending provider call cannot stall a contact lane.
|
|
11
16
|
- `POST /api/openchamber/llm/chat/completions` — `{ model, messages, stream?,
|
|
12
17
|
providerID?, modelID?, variant? }` → OpenAI `chat.completion` JSON.
|
|
13
|
-
This **public** gateway is **non-streaming**.
|
|
14
|
-
(`POST /generate` when present, otherwise throwaway `session.promptAsync`
|
|
15
|
-
plus idle wait) returns a full assistant turn. `stream: true` is rejected with
|
|
18
|
+
This **public** gateway is **non-streaming**. `stream: true` is rejected with
|
|
16
19
|
`validation_error` (HTTP 400). Do not emit fake SSE after the fact on this
|
|
17
|
-
route
|
|
18
|
-
The contact UI may stream later via a **server-internal** path only (below).
|
|
20
|
+
route. The contact UI may stream later via a **server-internal** path only.
|
|
19
21
|
|
|
20
22
|
`model` may be `providerID/modelID` or a bare `modelID` paired with `providerID`.
|
|
21
23
|
|
|
22
24
|
`variant` is an optional OpenCode model variant string. Null, omitted, or empty
|
|
23
25
|
values preserve the provider default. Other types fail validation before a model
|
|
24
|
-
call. A selected variant is forwarded unchanged
|
|
25
|
-
|
|
26
|
+
call. A selected variant is forwarded unchanged on the throwaway session's
|
|
27
|
+
`model` ref (both paths). OpenCode owns provider-specific variant
|
|
26
28
|
semantics. Variant selection changes inference configuration only; completion
|
|
27
29
|
output continues to include text parts and keeps reasoning parts private.
|
|
28
30
|
|
|
29
|
-
## Internals (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
31
|
+
## Internals (official `@opencode/client`)
|
|
32
|
+
|
|
33
|
+
Credentials stay in OpenCode. This module does not read `auth.json`, does not
|
|
34
|
+
call Anthropic/OpenAI/plugin SDKs, and does not use the `openai` npm package.
|
|
35
|
+
|
|
36
|
+
### Text path (no vision image bytes)
|
|
37
|
+
|
|
38
|
+
`session.generate` inside a throwaway session owned by the hidden deny-all
|
|
39
|
+
`openchamber-text` agent (same temp root, same deny-all verification and
|
|
40
|
+
`session.create` / `session.remove` lifecycle as the attachment path below):
|
|
41
|
+
|
|
42
|
+
1. Ensure `.opencode/agent/openchamber-text.md` and verify its final
|
|
43
|
+
permission rule is deny-all before creating the session.
|
|
44
|
+
2. `session.create({ location, agent: 'openchamber-text', model, metadata })`.
|
|
45
|
+
3. `session.generate({ sessionID, prompt })` → `{ text }`. One model call with
|
|
46
|
+
the session's provider context, no tool loop, no messages written. System
|
|
47
|
+
text is prepended into `prompt`.
|
|
48
|
+
4. Always `session.remove` in `finally` (interrupt first on abort/timeout).
|
|
49
|
+
|
|
50
|
+
`generate.text` (sessionless) is not used: session-scoped providers such as
|
|
51
|
+
OpenCode Go reject calls without a session (`x-opencode-session`).
|
|
52
|
+
`onTextDelta` is skipped honestly (no fake typewriter).
|
|
53
|
+
|
|
54
|
+
OpenCode loads a fresh location's config lazily, so `agent.get` can report the
|
|
55
|
+
agent as missing on the first read. Deny-all verification polls within a
|
|
56
|
+
bounded window (5s) before failing.
|
|
57
|
+
|
|
58
|
+
### Attachment / vision path
|
|
59
|
+
|
|
60
|
+
When the catalog marks the model vision-capable and the request includes image
|
|
61
|
+
data-URL file parts:
|
|
62
|
+
|
|
63
|
+
1. Ensure the throwaway temp directory agent markdown (`openchamber-llm`,
|
|
64
|
+
permissions array ending in `action:* resource:* effect:deny`).
|
|
65
|
+
2. `agent.get({ agentID: 'openchamber-llm', location })` and require the **final**
|
|
66
|
+
permissions rule to be deny-all. Failure → `llm_attachment_generation_unavailable`
|
|
67
|
+
**before** any `session.prompt`. Title is never treated as permission isolation.
|
|
68
|
+
3. `session.create({ location, agent, model: { id, providerID, variant? },
|
|
69
|
+
metadata: { openchamber: { llm: { purpose: 'chat-completions' } } } })`
|
|
70
|
+
(bare session id). Host `persistSessionMetadata` then commits the same
|
|
71
|
+
isolation patch; `onSystemSessionPersisted` drops the throwaway session
|
|
72
|
+
from the sidebar index. Create-time `metadata` is a hint only.
|
|
73
|
+
4. Optional `session.instructions.entry.put` for the system prompt.
|
|
74
|
+
5. Subscribe for `session.text.delta` (`data.sessionID` / `assistantMessageID` /
|
|
75
|
+
`ordinal` / `delta`) before prompt; unsubscribe in `finally`.
|
|
76
|
+
6. `session.prompt({ sessionID, text, files: [{ uri, name? }], delivery })`.
|
|
77
|
+
7. `session.wait` then `message.list({ order: 'desc' })`; verify newest assistant
|
|
78
|
+
`finish` / `error`; extract text content.
|
|
79
|
+
8. On timeout/abort: `session.interrupt({ continue: false })` and always
|
|
80
|
+
`session.remove` in `finally`.
|
|
81
|
+
|
|
82
|
+
Non-vision models keep the `[image: …]` description strategy and never forward
|
|
83
|
+
image bytes. Data URLs are strictly validated; a single attachment is capped at
|
|
84
|
+
20 MiB (plus existing inline text-file limits). An external/temp workspace that
|
|
85
|
+
OpenCode cannot see fails explicitly (`llm_attachment_generation_unavailable`).
|
|
86
|
+
|
|
87
|
+
`ensureLlmTempDirectory` keeps one process-wide throwaway root (`openchamber-llm-…`).
|
|
88
|
+
Concurrent first callers share a single `mkdtemp` inflight; every caller still
|
|
89
|
+
writes/refreshes `.opencode/agent/<name>.md` after settle so a mid-flight ensure
|
|
90
|
+
cannot skip a newer agent body. Concurrent warm refreshes serialize through one
|
|
91
|
+
write chain (last committed body wins; no interleaved partial writes).
|
|
92
|
+
`stopLlmTempDirectory` clears the singleton for shutdown/tests.
|
|
93
|
+
|
|
94
|
+
There is no `/generate` probe, no `tool.ids` deny map, and no `promptAsync` path.
|
|
95
|
+
There is no `config.providers` catalog path.
|
|
78
96
|
|
|
79
97
|
### Internal token callback (in-process only)
|
|
80
98
|
|
|
81
99
|
`generateOpenCodeText` / `createChatCompletion` accept optional
|
|
82
|
-
`onTextDelta(text: string)` and `globalEventHub
|
|
83
|
-
`globalMessageStreamHub` / `subscribeEvent` surface).
|
|
84
|
-
|
|
85
|
-
- **Throwaway session path:** after archive, before `promptAsync`, subscribe to
|
|
86
|
-
the hub. Forward real OpenCode `message.part.delta` events whose
|
|
87
|
-
`properties.sessionID` matches this throwaway session, `field` is `text`
|
|
88
|
-
(or omitted), and `messageID` matches the first assistant message locked for
|
|
89
|
-
that generate. Still wait for idle and return the **full** text (strip/parse
|
|
90
|
-
needs the complete string). Unsubscribe in `finally` so failed generates
|
|
91
|
-
cannot leak listeners.
|
|
92
|
-
- **Sessionless `/generate` JSON path:** cannot emit live deltas — skip
|
|
93
|
-
`onTextDelta` honestly (no fake typewriter of the completed string).
|
|
94
|
-
- **HTTP `POST .../chat/completions`:** does not pass these options and still
|
|
95
|
-
rejects `stream: true`. Token deltas never leave the server as SSE on that
|
|
96
|
-
route.
|
|
100
|
+
`onTextDelta(text: string)` and `globalEventHub`.
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Credentials stay in OpenCode. This module does not read `auth.json`, does not
|
|
104
|
-
call Anthropic/OpenAI/plugin SDKs, and does not use the `openai` npm package.
|
|
102
|
+
- **Attachment session path:** real `session.text.delta` tokens for this session.
|
|
103
|
+
- **`session.generate` text path:** cannot emit live deltas — skip `onTextDelta`.
|
|
104
|
+
- **HTTP `POST .../chat/completions`:** does not pass these options and still
|
|
105
|
+
rejects `stream: true`.
|
|
105
106
|
|
|
106
107
|
No connected provider → `no_provider` (HTTP 400). Upstream failure is never
|
|
107
108
|
an empty success. The 502 body includes `{ error, message }`.
|
|
@@ -117,6 +118,16 @@ application tool catalog and actual results.
|
|
|
117
118
|
The Assistant contact harness owns system prompt, OpenChamber transcript,
|
|
118
119
|
bubble splitting, and OpenChamber API tools (`assign_session`). Those tools
|
|
119
120
|
deliver through contact **cards**, not this completions payload. The gateway
|
|
120
|
-
stays a text generator: OpenCode coding tools stay denied
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
stays a text generator: OpenCode coding tools stay denied on both paths via the
|
|
122
|
+
verified agent permissions ruleset.
|
|
123
|
+
|
|
124
|
+
The gateway is also the single OpenCode-backed path for server utility calls
|
|
125
|
+
(`small-model`: commit messages, session titles, goal audits, scheduled-task
|
|
126
|
+
distill, TTS summaries, PR descriptions). No server module calls provider APIs
|
|
127
|
+
directly with OpenCode credentials.
|
|
128
|
+
|
|
129
|
+
Internal completion/generate calls accept an optional AbortSignal from the
|
|
130
|
+
contact continuation. It is combined with the generator deadline and passed to
|
|
131
|
+
upstream requests. Cancellation still runs throwaway-session cleanup
|
|
132
|
+
(`interrupt` + `remove`); it must not turn a late model response into a new
|
|
133
|
+
tool operation.
|
|
@@ -7,8 +7,10 @@ const listIncludesImage = (value) => Array.isArray(value)
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Vision capability projection.
|
|
10
|
-
* Prefer authoritative OpenCode
|
|
11
|
-
*
|
|
10
|
+
* Prefer authoritative OpenCode Model.capabilities (`input.image` boolean /
|
|
11
|
+
* `attachment`, or v2 list `input` containing `"image"`), then fall back to
|
|
12
|
+
* legacy modalities/input/attachment shapes used by older fixtures.
|
|
13
|
+
* Do not treat model titles or unrelated flags as vision isolation.
|
|
12
14
|
*/
|
|
13
15
|
export function modelAcceptsImages(model) {
|
|
14
16
|
if (!isRecord(model)) return false;
|
|
@@ -16,6 +18,7 @@ export function modelAcceptsImages(model) {
|
|
|
16
18
|
if (isRecord(model.capabilities.input) && typeof model.capabilities.input.image === 'boolean') {
|
|
17
19
|
return model.capabilities.input.image === true;
|
|
18
20
|
}
|
|
21
|
+
if (listIncludesImage(model.capabilities.input)) return true;
|
|
19
22
|
if (model.capabilities.attachment === true) return true;
|
|
20
23
|
}
|
|
21
24
|
if (isRecord(model.modalities) && listIncludesImage(model.modalities.input)) return true;
|
|
@@ -24,49 +27,76 @@ export function modelAcceptsImages(model) {
|
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
30
|
+
* Project official v2 provider.list + model.list into the LLM gateway catalog.
|
|
31
|
+
*
|
|
32
|
+
* ModelInfo.id is the external modelID used in generate/session model refs.
|
|
33
|
+
* ModelInfo.modelID is a separate internal field and must not be preferred.
|
|
29
34
|
*
|
|
30
35
|
* @param {{
|
|
31
|
-
* connected?: unknown,
|
|
32
36
|
* providers?: unknown,
|
|
37
|
+
* models?: unknown,
|
|
33
38
|
* }} source
|
|
34
39
|
*/
|
|
35
40
|
export function projectConnectedModels(source) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
41
|
-
const models = [];
|
|
42
|
-
const providers = [];
|
|
43
|
-
const list = Array.isArray(source?.providers) ? source.providers : [];
|
|
44
|
-
for (const provider of list) {
|
|
41
|
+
const providersIn = Array.isArray(source?.providers) ? source.providers : [];
|
|
42
|
+
const modelsIn = Array.isArray(source?.models) ? source.models : [];
|
|
43
|
+
|
|
44
|
+
const providerMeta = new Map();
|
|
45
|
+
for (const provider of providersIn) {
|
|
45
46
|
if (!isRecord(provider)) continue;
|
|
46
47
|
const providerID = stringID(provider.id);
|
|
47
|
-
if (!providerID
|
|
48
|
-
|
|
49
|
-
const rawModels = isRecord(provider.models) ? Object.values(provider.models) : [];
|
|
50
|
-
for (const model of rawModels) {
|
|
51
|
-
if (!isRecord(model)) continue;
|
|
52
|
-
const modelID = stringID(model.id);
|
|
53
|
-
if (!modelID) continue;
|
|
54
|
-
const entry = {
|
|
55
|
-
providerID,
|
|
56
|
-
modelID,
|
|
57
|
-
name: stringID(model.name) || modelID,
|
|
58
|
-
acceptsImages: modelAcceptsImages(model),
|
|
59
|
-
};
|
|
60
|
-
models.push(entry);
|
|
61
|
-
providerModels.push({ id: modelID, name: entry.name });
|
|
62
|
-
}
|
|
63
|
-
providers.push({
|
|
48
|
+
if (!providerID) continue;
|
|
49
|
+
providerMeta.set(providerID, {
|
|
64
50
|
id: providerID,
|
|
65
51
|
name: stringID(provider.name) || providerID,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const models = [];
|
|
56
|
+
const modelsByProvider = new Map();
|
|
57
|
+
for (const model of modelsIn) {
|
|
58
|
+
if (!isRecord(model)) continue;
|
|
59
|
+
const providerID = stringID(model.providerID);
|
|
60
|
+
// External model id for session model refs.
|
|
61
|
+
const modelID = stringID(model.id);
|
|
62
|
+
if (!providerID || !modelID) continue;
|
|
63
|
+
if (providerMeta.size > 0 && !providerMeta.has(providerID)) continue;
|
|
64
|
+
const name = stringID(model.name) || modelID;
|
|
65
|
+
const entry = {
|
|
66
|
+
providerID,
|
|
67
|
+
modelID,
|
|
68
|
+
name,
|
|
69
|
+
acceptsImages: modelAcceptsImages(model),
|
|
70
|
+
};
|
|
71
|
+
models.push(entry);
|
|
72
|
+
if (!modelsByProvider.has(providerID)) modelsByProvider.set(providerID, []);
|
|
73
|
+
modelsByProvider.get(providerID).push({ id: modelID, name });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const connected = [];
|
|
77
|
+
const providers = [];
|
|
78
|
+
const providerIDs = providerMeta.size > 0
|
|
79
|
+
? [...providerMeta.keys()]
|
|
80
|
+
: [...modelsByProvider.keys()];
|
|
81
|
+
for (const providerID of providerIDs) {
|
|
82
|
+
const meta = providerMeta.get(providerID) || { id: providerID, name: providerID };
|
|
83
|
+
const providerModels = modelsByProvider.get(providerID) || [];
|
|
84
|
+
if (providerModels.length === 0 && providerMeta.size > 0) {
|
|
85
|
+
// Provider listed with zero models is still "connected" but contributes no entries.
|
|
86
|
+
connected.push(providerID);
|
|
87
|
+
providers.push({ id: meta.id, name: meta.name, models: [] });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (providerModels.length === 0) continue;
|
|
91
|
+
connected.push(providerID);
|
|
92
|
+
providers.push({
|
|
93
|
+
id: meta.id,
|
|
94
|
+
name: meta.name,
|
|
66
95
|
models: providerModels,
|
|
67
96
|
});
|
|
68
97
|
}
|
|
69
|
-
|
|
98
|
+
|
|
99
|
+
return { connected, providers, models };
|
|
70
100
|
}
|
|
71
101
|
|
|
72
102
|
export function parseModelRef(model, providerID, modelID) {
|
|
@@ -92,7 +122,7 @@ export function isConnectedModel(catalog, providerID, modelID) {
|
|
|
92
122
|
return catalog.models.some((entry) => entry.providerID === providerID && entry.modelID === modelID);
|
|
93
123
|
}
|
|
94
124
|
|
|
95
|
-
/** Bound provider/
|
|
125
|
+
/** Bound provider/model catalog loads so a pending SDK call cannot stall a contact lane. */
|
|
96
126
|
export const CONNECTED_CATALOG_TIMEOUT_MS = 8_000;
|
|
97
127
|
|
|
98
128
|
const isAbortError = (error) => (
|
|
@@ -101,18 +131,41 @@ const isAbortError = (error) => (
|
|
|
101
131
|
|| (typeof error?.message === 'string' && /aborted|timed out/i.test(error.message))
|
|
102
132
|
);
|
|
103
133
|
|
|
134
|
+
const resolveCatalogLocation = (options) => {
|
|
135
|
+
if (!options || typeof options !== 'object') return undefined;
|
|
136
|
+
if (isRecord(options.location)) return options.location;
|
|
137
|
+
if (options.directory || options.workspace) {
|
|
138
|
+
return {
|
|
139
|
+
...(options.directory ? { directory: options.directory } : {}),
|
|
140
|
+
...(options.workspace ? { workspace: options.workspace } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
};
|
|
145
|
+
|
|
104
146
|
/**
|
|
105
|
-
* Load the connected catalog from an
|
|
147
|
+
* Load the connected catalog from an official `@opencode/client`.
|
|
148
|
+
* Uses model.list + provider.list only (no config.providers).
|
|
106
149
|
* Failure is distinct from a successful empty catalog.
|
|
107
|
-
*
|
|
150
|
+
*
|
|
151
|
+
* Real client returns `{ location, data }` and throws declared JSON errors
|
|
152
|
+
* (no `{ error }` envelope). Uses a bounded AbortSignal (default 8s).
|
|
108
153
|
*
|
|
109
154
|
* @param {object} client
|
|
110
|
-
* @param {{
|
|
155
|
+
* @param {{
|
|
156
|
+
* location?: { directory?: string, workspace?: string },
|
|
157
|
+
* directory?: string,
|
|
158
|
+
* workspace?: string,
|
|
159
|
+
* signal?: AbortSignal,
|
|
160
|
+
* timeoutMs?: number,
|
|
161
|
+
* }} [options]
|
|
111
162
|
*/
|
|
112
163
|
export async function loadConnectedCatalog(client, options = {}) {
|
|
113
164
|
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
|
|
114
165
|
? Math.trunc(options.timeoutMs)
|
|
115
166
|
: CONNECTED_CATALOG_TIMEOUT_MS;
|
|
167
|
+
const location = resolveCatalogLocation(options);
|
|
168
|
+
const request = location ? { location } : undefined;
|
|
116
169
|
const parentSignal = options.signal;
|
|
117
170
|
const controller = new AbortController();
|
|
118
171
|
const onParentAbort = () => {
|
|
@@ -139,21 +192,34 @@ export async function loadConnectedCatalog(client, options = {}) {
|
|
|
139
192
|
|
|
140
193
|
const requestOptions = { signal: controller.signal };
|
|
141
194
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
195
|
+
let providersResult;
|
|
196
|
+
let modelsResult;
|
|
197
|
+
try {
|
|
198
|
+
[providersResult, modelsResult] = await Promise.all([
|
|
199
|
+
client.provider.list(request, requestOptions),
|
|
200
|
+
client.model.list(request, requestOptions),
|
|
201
|
+
]);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (controller.signal.aborted || isAbortError(error)) {
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
const upstream = new Error(
|
|
207
|
+
typeof error?.message === 'string' && error.message.trim()
|
|
208
|
+
? error.message
|
|
209
|
+
: 'OpenCode provider catalog is unavailable',
|
|
210
|
+
);
|
|
211
|
+
upstream.code = 'upstream_error';
|
|
212
|
+
upstream.cause = error;
|
|
213
|
+
throw upstream;
|
|
147
214
|
}
|
|
148
|
-
|
|
149
|
-
if (configured?.error || !isRecord(configured?.data)) {
|
|
215
|
+
if (!Array.isArray(providersResult?.data) || !Array.isArray(modelsResult?.data)) {
|
|
150
216
|
const error = new Error('OpenCode provider catalog is unavailable');
|
|
151
217
|
error.code = 'upstream_error';
|
|
152
218
|
throw error;
|
|
153
219
|
}
|
|
154
220
|
return projectConnectedModels({
|
|
155
|
-
|
|
156
|
-
|
|
221
|
+
providers: providersResult.data,
|
|
222
|
+
models: modelsResult.data,
|
|
157
223
|
});
|
|
158
224
|
} catch (error) {
|
|
159
225
|
if (controller.signal.aborted || isAbortError(error)) {
|