@vantran-se/openchamber-web 1.24.4 → 2.0.2
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 +2 -8
- package/bin/lib/commands-connect-url.js +3 -8
- package/bin/lib/commands-tunnel.js +3 -0
- package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-B82Oye3V.js} +1 -1
- package/dist/assets/DiffView-DJLmWn6Z.js +4 -0
- package/dist/assets/FilesView-DjksmZRo.js +17 -0
- package/dist/assets/{GitView-Byu2DY7s.js → GitView-DiUsUIgr.js} +4 -4
- package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-CYwWYLr3.js} +1 -1
- package/dist/assets/GuestSurfacePane-CmctyRI4.js +1 -0
- package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-DOHHcbgJ.js} +1 -1
- package/dist/assets/LinearIssuesView-DaqnylcV.js +1 -0
- package/dist/assets/MarkdownImageGallery-HJNvWt7L.js +1 -0
- package/dist/assets/MarkdownRendererImpl-nDZRGg2I.js +12 -0
- package/dist/assets/OnboardingScreen-C2B5Bsof.js +1 -0
- package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-C4FLBjpt.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-C_1S9c9n.js} +1 -1
- package/dist/assets/PlanView-_eZvb-G5.js +5 -0
- package/dist/assets/PluginPane-sM8fcZIV.js +3 -0
- package/dist/assets/SettingsView-BRfr46hb.js +29 -0
- package/dist/assets/SettingsWindow-CcUDwQHg.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-CrN1ZGwl.js +1 -0
- package/dist/assets/ToolOutputDialog-Cij4jI09.js +15 -0
- package/dist/assets/ToolPartDiffPreview-BXzwQ-JY.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-DSWTLJNH.js} +4 -4
- package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-CpUUgY8H.js} +2 -2
- package/dist/assets/appThemeRegistry-B5tHTkR8.js +1 -0
- package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-D7D8qV8p.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-cq9uQ30i.js} +1 -1
- package/dist/assets/de-CUO1-KqF.js +10 -0
- package/dist/assets/es-BlbC6sGJ.js +16 -0
- package/dist/assets/fr-D2nwe_0W.js +14 -0
- package/dist/assets/index-BQOoYIOu.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D0obJ2p8.js} +1 -1
- package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-DB3_rjyW.js} +1 -1
- package/dist/assets/ja-CA7vnDsZ.js +16 -0
- package/dist/assets/ko-Chy6a9l-.js +16 -0
- package/dist/assets/main-B9JFK3vF.js +896 -0
- package/dist/assets/main-Bhiqnrhd.js +2 -0
- package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-tfqOFaBd.js} +4 -4
- package/dist/assets/miniChat-B0UOL5zA.js +2 -0
- package/dist/assets/mobile-BJppFIEo.js +2 -0
- package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-Db6KuwCP.js} +3 -3
- package/dist/assets/pl-CTR5A_1P.js +16 -0
- package/dist/assets/pt-BR-BE1O1KEx.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-Yz5sfoFh.js +2 -0
- package/dist/assets/renderMobileApp-QQ-yB8aY.js +25 -0
- package/dist/assets/runtimeEndpointReset-DKDdzRzm.js +27 -0
- package/dist/assets/scheduledTasksApi-C5sCuNqf.js +1 -0
- package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-xURoo5t_.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-BZLGuYTU.js} +1 -1
- package/dist/assets/tr-Bh5vK6cW.js +7 -0
- package/dist/assets/uk-CLDkZFy_.js +16 -0
- package/dist/assets/useAppFontEffects-BxrtCnfR.js +7337 -0
- package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CeXuR-SR.js} +2 -2
- package/dist/assets/useRootScrollLock-DgWxezc1.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-DOuaQUZ0.js} +2 -2
- package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
- package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
- package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
- package/dist/assets/{web-components-DxX8bZ1o.js → web-components-63H1h21j.js} +2 -2
- package/dist/assets/zh-CN-DODgcmYj.js +16 -0
- package/dist/assets/zh-TW-ByNfIwLA.js +16 -0
- package/dist/index.html +13 -7
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +6 -5
- package/server/index.d.ts +1 -0
- package/server/index.js +246 -91
- package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
- package/server/lib/agent-tool/runtime.js +135 -54
- package/server/lib/agent-tool/runtime.test.js +155 -87
- package/server/lib/browser-control/DOCUMENTATION.md +37 -2
- package/server/lib/browser-control/provider.js +217 -0
- package/server/lib/browser-control/provider.test.js +251 -0
- package/server/lib/context-obligatory/runtime.js +61 -50
- package/server/lib/context-obligatory/runtime.test.js +62 -162
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +68 -57
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-hub.js +10 -1
- package/server/lib/event-stream/global-hub.test.js +27 -26
- package/server/lib/event-stream/index.js +6 -0
- package/server/lib/event-stream/protocol.js +26 -19
- package/server/lib/event-stream/protocol.test.js +18 -7
- package/server/lib/event-stream/rebind.test.js +5 -5
- package/server/lib/event-stream/runtime.test.js +17 -15
- package/server/lib/event-stream/translate-v2.js +313 -0
- package/server/lib/event-stream/translate-v2.test.js +209 -0
- package/server/lib/event-stream/upstream-reader.test.js +16 -16
- package/server/lib/fs/DOCUMENTATION.md +4 -2
- package/server/lib/fs/byte-range.js +35 -0
- package/server/lib/fs/byte-range.test.js +31 -0
- package/server/lib/fs/routes.js +47 -42
- package/server/lib/fs/routes.test.js +123 -18
- package/server/lib/git/service.js +12 -0
- package/server/lib/github/DOCUMENTATION.md +1 -1
- package/server/lib/github/pr-status.js +22 -2
- package/server/lib/github/pr-status.test.js +27 -14
- package/server/lib/guests/DOCUMENTATION.md +13 -0
- package/server/lib/guests/catalog.js +12 -0
- package/server/lib/guests/catalog.test.js +33 -0
- package/server/lib/guests/routes.js +10 -1
- package/server/lib/guests/service.js +192 -14
- package/server/lib/guests/service.test.js +201 -1
- package/server/lib/guests/surface.js +630 -0
- package/server/lib/guests/surface.test.js +486 -0
- package/server/lib/markdown-image-grants/routes.js +18 -5
- package/server/lib/markdown-image-grants/routes.test.js +9 -6
- package/server/lib/message-queue/DOCUMENTATION.md +33 -22
- package/server/lib/message-queue/runtime.js +155 -100
- package/server/lib/message-queue/runtime.test.js +168 -82
- package/server/lib/notifications/DOCUMENTATION.md +28 -0
- package/server/lib/notifications/control-stream.http.test.js +68 -0
- package/server/lib/notifications/emitter-runtime.js +15 -0
- package/server/lib/notifications/emitter-runtime.test.js +32 -0
- package/server/lib/notifications/push-runtime.js +5 -1
- package/server/lib/notifications/push-runtime.test.js +2 -2
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +115 -26
- package/server/lib/notifications/template-runtime.js +13 -15
- package/server/lib/notifications/template-runtime.test.js +16 -9
- package/server/lib/notifications/trigger-runtime.test.js +187 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
- package/server/lib/openchamber-control/actions.js +3 -2
- package/server/lib/openchamber-control/error.js +1 -1
- package/server/lib/openchamber-control/file-open.js +59 -0
- package/server/lib/openchamber-control/file-open.test.js +57 -0
- package/server/lib/openchamber-control/routes.test.js +12 -0
- package/server/lib/openchamber-control/service.js +102 -49
- package/server/lib/openchamber-control/service.test.js +84 -44
- package/server/lib/openchamber-sessions/archive-store.js +224 -0
- package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
- package/server/lib/openchamber-sessions/opencode-client.js +22 -0
- package/server/lib/openchamber-sessions/routes.js +398 -328
- package/server/lib/openchamber-sessions/routes.test.js +777 -740
- package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
- package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
- package/server/lib/opencode/DOCUMENTATION.md +449 -29
- package/server/lib/opencode/agents.js +233 -327
- package/server/lib/opencode/auth.js +41 -44
- package/server/lib/opencode/auth.test.js +92 -0
- package/server/lib/opencode/cli-upgrade.d.ts +4 -0
- package/server/lib/opencode/cli-upgrade.js +18 -0
- package/server/lib/opencode/cli-upgrade.test.js +36 -0
- package/server/lib/opencode/commands.js +164 -145
- package/server/lib/opencode/compatibility.d.ts +16 -0
- package/server/lib/opencode/compatibility.js +88 -0
- package/server/lib/opencode/compatibility.test.js +62 -0
- package/server/lib/opencode/config-entity-routes.js +58 -29
- package/server/lib/opencode/config-entity-v2.test.js +290 -0
- package/server/lib/opencode/config-mutation-response.js +17 -7
- package/server/lib/opencode/config-mutation-response.test.js +4 -7
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.d.ts +183 -0
- package/server/lib/opencode/config-v2.js +801 -0
- package/server/lib/opencode/config-v2.test.js +466 -0
- package/server/lib/opencode/credential-db.js +144 -0
- package/server/lib/opencode/credential-db.test.js +104 -0
- package/server/lib/opencode/env-runtime.js +48 -9
- package/server/lib/opencode/env-runtime.test.js +45 -0
- package/server/lib/opencode/feature-routes-runtime.js +15 -5
- package/server/lib/opencode/lifecycle-process.test.js +7 -4
- package/server/lib/opencode/lifecycle.js +85 -30
- package/server/lib/opencode/lifecycle.test.js +80 -8
- package/server/lib/opencode/login-shell-env.js +27 -0
- package/server/lib/opencode/managed-config-file.js +119 -0
- package/server/lib/opencode/managed-config-file.test.js +152 -0
- package/server/lib/opencode/managed-plugin-config.js +33 -13
- package/server/lib/opencode/mcp.js +43 -149
- package/server/lib/opencode/network-runtime.js +6 -7
- package/server/lib/opencode/openchamber-routes.test.js +2 -5
- package/server/lib/opencode/plugin-routes.js +3 -3
- package/server/lib/opencode/plugin-routes.test.js +6 -24
- package/server/lib/opencode/plugins.js +91 -40
- package/server/lib/opencode/plugins.test.js +28 -15
- package/server/lib/opencode/providers.js +63 -83
- package/server/lib/opencode/providers.test.js +128 -59
- package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
- package/server/lib/opencode/proxy.js +179 -129
- package/server/lib/opencode/pwa-manifest-routes.js +4 -2
- package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
- package/server/lib/opencode/response-envelope.js +14 -0
- package/server/lib/opencode/response-envelope.test.js +23 -0
- package/server/lib/opencode/routes-upgrade.test.js +73 -116
- package/server/lib/opencode/routes.js +121 -428
- package/server/lib/opencode/server-utils-runtime.js +10 -4
- package/server/lib/opencode/session-activity.js +78 -0
- package/server/lib/opencode/session-runtime.js +21 -10
- package/server/lib/opencode/session-runtime.test.js +9 -9
- package/server/lib/opencode/settings-helpers.js +9 -9
- package/server/lib/opencode/settings-helpers.test.js +7 -12
- package/server/lib/opencode/settings-registry.json +6 -8
- package/server/lib/opencode/settings-runtime.js +18 -1
- package/server/lib/opencode/settings-runtime.test.js +31 -2
- package/server/lib/opencode/shared.js +54 -22
- package/server/lib/opencode/shared.test.js +8 -4
- package/server/lib/opencode/shutdown-runtime.js +54 -7
- package/server/lib/opencode/shutdown-runtime.test.js +224 -30
- package/server/lib/opencode/skill-routes.js +23 -24
- package/server/lib/opencode/skills.js +7 -5
- package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
- package/server/lib/opencode/upgrade-capability.test.js +1 -1
- package/server/lib/opencode/v1-migration-topup.js +288 -0
- package/server/lib/opencode/v1-migration-topup.test.js +266 -0
- package/server/lib/opencode/v2-install.d.ts +8 -0
- package/server/lib/opencode/v2-install.js +158 -0
- package/server/lib/opencode/v2-install.test.js +124 -0
- package/server/lib/opencode/watcher.js +10 -3
- package/server/lib/opencode/watcher.test.js +17 -21
- package/server/lib/opencode/websearch-config.js +66 -0
- package/server/lib/package-manager.js +55 -68
- package/server/lib/package-manager.test.js +241 -75
- package/server/lib/permission-auto-accept/runtime.js +38 -22
- package/server/lib/permission-auto-accept/runtime.test.js +39 -17
- package/server/lib/quota/DOCUMENTATION.md +4 -0
- package/server/lib/quota/providers/xai.js +4 -4
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +7 -0
- package/server/lib/routing/DOCUMENTATION.md +52 -21
- package/server/lib/routing/defaults.js +23 -6
- package/server/lib/routing/jev.js +17 -7
- package/server/lib/routing/routes.http.test.js +68 -33
- package/server/lib/routing/routes.js +65 -22
- package/server/lib/routing/runtime.js +145 -48
- package/server/lib/routing/runtime.test.js +79 -55
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
- package/server/lib/scheduled-tasks/runtime.js +81 -74
- package/server/lib/scheduled-tasks/runtime.test.js +53 -1
- package/server/lib/session-assist/DOCUMENTATION.md +8 -3
- package/server/lib/session-assist/context.js +122 -46
- package/server/lib/session-assist/context.test.js +117 -49
- package/server/lib/session-assist/runtime.js +52 -22
- package/server/lib/session-assist/runtime.test.js +145 -173
- package/server/lib/session-goal/DOCUMENTATION.md +23 -15
- package/server/lib/session-goal/create.js +9 -12
- package/server/lib/session-goal/create.test.js +27 -28
- package/server/lib/session-goal/runtime.js +164 -77
- package/server/lib/session-goal/runtime.test.js +271 -519
- package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
- package/server/lib/session-knowledge/runtime.js +59 -41
- package/server/lib/session-knowledge/runtime.test.js +90 -20
- package/server/lib/small-model/DOCUMENTATION.md +154 -208
- package/server/lib/small-model/client.js +113 -0
- package/server/lib/small-model/index.js +296 -158
- package/server/lib/small-model/index.test.js +423 -256
- package/server/lib/small-model/routes.js +1 -1
- package/server/lib/spaces/DOCUMENTATION.md +807 -0
- package/server/lib/spaces/code-in-bait.js +185 -0
- package/server/lib/spaces/code-in.js +641 -0
- package/server/lib/spaces/code-in.test.js +1048 -0
- package/server/lib/spaces/errors.js +8 -0
- package/server/lib/spaces/exec-http.js +94 -0
- package/server/lib/spaces/gatekeeper-channel.js +202 -0
- package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
- package/server/lib/spaces/gatekeeper-program.cjs +914 -0
- package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
- package/server/lib/spaces/hardening.js +457 -0
- package/server/lib/spaces/hardening.test.js +368 -0
- package/server/lib/spaces/host-git.js +127 -0
- package/server/lib/spaces/host-git.test.js +137 -0
- package/server/lib/spaces/labels.js +234 -0
- package/server/lib/spaces/labels.test.js +219 -0
- package/server/lib/spaces/layout.js +184 -0
- package/server/lib/spaces/layout.test.js +30 -0
- package/server/lib/spaces/manager.js +82 -0
- package/server/lib/spaces/manager.test.js +114 -0
- package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
- package/server/lib/spaces/places/contract-suite.js +156 -0
- package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
- package/server/lib/spaces/places/contract.test.js +6 -0
- package/server/lib/spaces/places/docker-engine.js +126 -0
- package/server/lib/spaces/places/docker-live-support.js +202 -0
- package/server/lib/spaces/places/docker-tools.js +186 -0
- package/server/lib/spaces/places/docker.js +587 -0
- package/server/lib/spaces/places/docker.test.js +1610 -0
- package/server/lib/spaces/places/escape-suite.js +1018 -0
- package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
- package/server/lib/spaces/places/fake-docker.js +375 -0
- package/server/lib/spaces/places/memory-place.js +103 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
- package/server/lib/spaces/places/registry.js +43 -0
- package/server/lib/spaces/places/registry.test.js +50 -0
- package/server/lib/spaces/places/server.docker.live.test.js +225 -0
- package/server/lib/spaces/run-command.js +261 -0
- package/server/lib/spaces/run-command.test.js +396 -0
- package/server/lib/spaces/space-server.js +190 -0
- package/server/lib/spaces/space-server.test.js +335 -0
- package/server/lib/spaces/tools-filler.js +82 -0
- package/server/lib/spaces/tools-filler.test.js +148 -0
- package/server/lib/spaces/tools-pack.js +49 -0
- package/server/lib/spaces/tools-pack.test.js +57 -0
- package/server/lib/spaces/tools.js +138 -0
- package/server/lib/spaces/tools.test.js +153 -0
- package/server/lib/tts/openai-sdk.js +11 -0
- package/server/lib/tts/service.js +7 -5
- package/server/lib/tts/stt.js +3 -2
- package/server/lib/tunnels/DOCUMENTATION.md +2 -0
- package/server/lib/tunnels/routes.js +7 -0
- package/server/lib/tunnels/routes.test.js +55 -0
- package/server/lib/ui-auth/ui-auth.js +38 -2
- package/server/lib/ui-auth/ui-auth.test.js +42 -0
- package/server/lib/ui-auth/ui-passkeys.js +12 -10
- package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
- package/server/lib/walkthrough/pull-request.js +61 -8
- package/server/lib/walkthrough/pull-request.test.js +54 -1
- package/server/lib/walkthrough/routes.js +20 -0
- package/server/lib/walkthrough/routes.test.js +19 -0
- package/server/opencode-proxy.test.js +190 -166
- package/server/opencode-worktree-readiness.test.js +5 -4
- package/dist/assets/DiffView-B2CD9wC8.js +0 -4
- package/dist/assets/FilesView-C4vMFclc.js +0 -13
- package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
- package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
- package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
- package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
- package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
- package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
- package/dist/assets/SettingsView-CSinuH-1.js +0 -29
- package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
- package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
- package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
- package/dist/assets/de-Bro6_zyl.js +0 -10
- package/dist/assets/es-Bb9XQZvd.js +0 -16
- package/dist/assets/fr-Dgfzn-IL.js +0 -14
- package/dist/assets/index-CtQNmqUL.js +0 -1
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/index-Dqc6USl3.js +0 -40
- package/dist/assets/ja-BYsakGFh.js +0 -16
- package/dist/assets/ko-BSaQz9aN.js +0 -16
- package/dist/assets/main-B62_pjL5.js +0 -2
- package/dist/assets/main-BEdcOI2P.js +0 -896
- package/dist/assets/miniChat-LT8Rqofr.js +0 -2
- package/dist/assets/mobile-CqarEynh.js +0 -2
- package/dist/assets/pl-OIviPdoj.js +0 -16
- package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
- package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
- package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
- package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
- package/dist/assets/tr-CQI-KPre.js +0 -7
- package/dist/assets/uk-CqW7R4DA.js +0 -16
- package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
- package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
- package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
- package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
- package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
- package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
- package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
- package/server/lib/routing/feature-flag.js +0 -11
- package/server/lib/small-model/call.js +0 -851
- package/server/lib/small-model/call.test.js +0 -1292
- package/server/lib/small-model/catalog.js +0 -13
- package/server/lib/small-model/resolve.js +0 -131
- package/server/lib/small-model/resolve.test.js +0 -197
- package/server/lib/small-model/runtime-providers.js +0 -164
- package/server/lib/small-model/runtime-providers.test.js +0 -118
- package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
- package/server/lib/system-prompt/runtime.js +0 -49
- package/server/lib/system-prompt/runtime.test.js +0 -76
- package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { getModelsMetadata } from '../opencode/models-metadata.js';
|
|
2
|
-
|
|
3
|
-
// The models.dev catalog is shared with the /api/openchamber/models-metadata
|
|
4
|
-
// route through one in-process cache — no extra fetches, no cache files.
|
|
5
|
-
export async function getModelCatalog() {
|
|
6
|
-
const { metadata } = await getModelsMetadata();
|
|
7
|
-
return metadata;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function getCatalogProvider(catalog, providerID) {
|
|
11
|
-
const entry = catalog?.[providerID];
|
|
12
|
-
return entry && typeof entry === 'object' ? entry : null;
|
|
13
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { getCatalogProvider } from './catalog.js';
|
|
2
|
-
|
|
3
|
-
// Mirrors OpenCode's getSmallModel fallback chain:
|
|
4
|
-
// 1. `small_model` from the merged config layers ("provider/model").
|
|
5
|
-
// 2. GitHub Copilot's hidden utility models when Copilot is logged in.
|
|
6
|
-
// 3. Family-priority scan of the authenticated providers' catalog models.
|
|
7
|
-
const FAMILY_PRIORITY = ['gemini-flash', 'gpt-nano', 'claude-haiku'];
|
|
8
|
-
const COPILOT_UTILITY_MODELS = ['gpt-5.4-nano', 'gpt-4.1', 'gpt-4o', 'gpt-4o-mini'];
|
|
9
|
-
// The ChatGPT-plan codex backend only accepts a small allowlist of models
|
|
10
|
-
// (nano/API-key models are rejected with 400) — this is its cheapest one.
|
|
11
|
-
const OPENAI_OAUTH_SMALL_MODEL = 'gpt-5.4-mini';
|
|
12
|
-
|
|
13
|
-
const AUTH_PROVIDER_ALIASES = {
|
|
14
|
-
'github-copilot': ['github-copilot', 'copilot'],
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export function getAuthEntryForProvider(auth, providerID) {
|
|
18
|
-
const aliases = AUTH_PROVIDER_ALIASES[providerID] || [providerID];
|
|
19
|
-
for (const alias of aliases) {
|
|
20
|
-
const entry = auth?.[alias];
|
|
21
|
-
if (entry && typeof entry === 'object') {
|
|
22
|
-
return entry;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function isUsableAuthEntry(entry) {
|
|
29
|
-
if (!entry || typeof entry !== 'object') return false;
|
|
30
|
-
if (entry.type === 'api') return typeof entry.key === 'string' && entry.key.length > 0;
|
|
31
|
-
if (entry.type === 'oauth') {
|
|
32
|
-
return (typeof entry.access === 'string' && entry.access.length > 0)
|
|
33
|
-
|| (typeof entry.refresh === 'string' && entry.refresh.length > 0);
|
|
34
|
-
}
|
|
35
|
-
if (entry.type === 'wellknown') return typeof entry.token === 'string' && entry.token.length > 0;
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function parseModelRef(value) {
|
|
40
|
-
if (typeof value !== 'string') return null;
|
|
41
|
-
const trimmed = value.trim();
|
|
42
|
-
const slash = trimmed.indexOf('/');
|
|
43
|
-
if (slash <= 0 || slash === trimmed.length - 1) return null;
|
|
44
|
-
return {
|
|
45
|
-
providerID: trimmed.slice(0, slash),
|
|
46
|
-
modelID: trimmed.slice(slash + 1),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const pickByFamily = (models, family) => {
|
|
51
|
-
const matches = Object.values(models)
|
|
52
|
-
.filter((model) => model && typeof model === 'object' && model.family === family);
|
|
53
|
-
if (matches.length === 0) return null;
|
|
54
|
-
matches.sort((a, b) => String(b.release_date || '').localeCompare(String(a.release_date || '')));
|
|
55
|
-
return matches[0];
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Small-model candidates within ONE provider, by family priority. Copilot and
|
|
59
|
-
// ChatGPT-plan OpenAI have fixed small models that never appear in the
|
|
60
|
-
// catalog; everyone else is scanned through the catalog families.
|
|
61
|
-
const pickWithinProvider = (providerID, auth, catalog, family) => {
|
|
62
|
-
if (providerID === 'openai' && auth.openai?.type === 'oauth') {
|
|
63
|
-
return family === 'gpt-nano'
|
|
64
|
-
? { providerID, modelID: OPENAI_OAUTH_SMALL_MODEL, source: 'codex-small' }
|
|
65
|
-
: null;
|
|
66
|
-
}
|
|
67
|
-
if (providerID === 'github-copilot') {
|
|
68
|
-
return family === 'gpt-nano'
|
|
69
|
-
? { providerID, modelID: COPILOT_UTILITY_MODELS[0], source: 'copilot-utility' }
|
|
70
|
-
: null;
|
|
71
|
-
}
|
|
72
|
-
const provider = getCatalogProvider(catalog, providerID);
|
|
73
|
-
if (!provider || !provider.models || typeof provider.models !== 'object') return null;
|
|
74
|
-
const model = pickByFamily(provider.models, family);
|
|
75
|
-
return model?.id ? { providerID, modelID: model.id, source: 'family-scan' } : null;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export function resolveSmallModel({ auth, catalog, settingsSmallModel, configSmallModel, preferredProviderID, preferredModelID }) {
|
|
79
|
-
// OpenChamber's own setting (Settings → Sessions → Small Model override)
|
|
80
|
-
// outranks everything, including the OpenCode config.
|
|
81
|
-
const fromSettings = parseModelRef(settingsSmallModel);
|
|
82
|
-
if (fromSettings) {
|
|
83
|
-
return { ...fromSettings, source: 'settings' };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const explicit = parseModelRef(configSmallModel);
|
|
87
|
-
if (explicit) {
|
|
88
|
-
return { ...explicit, source: 'config' };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Like OpenCode: when the caller has a session context, the utility call
|
|
92
|
-
// stays on the session's provider. Scan its families for a small model,
|
|
93
|
-
// otherwise run on the session's own model — never silently switch to a
|
|
94
|
-
// different provider's subscription.
|
|
95
|
-
const preferred = typeof preferredProviderID === 'string' && preferredProviderID
|
|
96
|
-
? preferredProviderID
|
|
97
|
-
: null;
|
|
98
|
-
if (preferred && isUsableAuthEntry(getAuthEntryForProvider(auth, preferred))) {
|
|
99
|
-
for (const family of FAMILY_PRIORITY) {
|
|
100
|
-
const match = pickWithinProvider(preferred, auth, catalog, family);
|
|
101
|
-
if (match) return match;
|
|
102
|
-
}
|
|
103
|
-
if (typeof preferredModelID === 'string' && preferredModelID) {
|
|
104
|
-
return { providerID: preferred, modelID: preferredModelID, source: 'session-model' };
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// No session context (or its provider has no usable login): scan all
|
|
109
|
-
// authenticated providers by family priority.
|
|
110
|
-
const authedProviders = Object.keys(auth || {}).filter((providerID) =>
|
|
111
|
-
providerID !== preferred && isUsableAuthEntry(auth[providerID]));
|
|
112
|
-
|
|
113
|
-
for (const family of FAMILY_PRIORITY) {
|
|
114
|
-
for (const providerID of authedProviders) {
|
|
115
|
-
const match = pickWithinProvider(providerID, auth, catalog, family);
|
|
116
|
-
if (match) return match;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Copilot's utility fallback for legacy auth aliases the loop above missed.
|
|
121
|
-
const copilotEntry = getAuthEntryForProvider(auth, 'github-copilot');
|
|
122
|
-
if (isUsableAuthEntry(copilotEntry)) {
|
|
123
|
-
return {
|
|
124
|
-
providerID: 'github-copilot',
|
|
125
|
-
modelID: COPILOT_UTILITY_MODELS[0],
|
|
126
|
-
source: 'copilot-utility',
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'bun:test';
|
|
2
|
-
import { resolveSmallModel, parseModelRef, isUsableAuthEntry } from './resolve.js';
|
|
3
|
-
|
|
4
|
-
const catalog = {
|
|
5
|
-
google: {
|
|
6
|
-
id: 'google',
|
|
7
|
-
models: {
|
|
8
|
-
'gemini-2.5-flash': { id: 'gemini-2.5-flash', family: 'gemini-flash', release_date: '2025-06-01' },
|
|
9
|
-
'gemini-2.0-flash': { id: 'gemini-2.0-flash', family: 'gemini-flash', release_date: '2024-12-01' },
|
|
10
|
-
'gemini-2.5-pro': { id: 'gemini-2.5-pro', family: 'gemini-pro', release_date: '2025-06-01' },
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
anthropic: {
|
|
14
|
-
id: 'anthropic',
|
|
15
|
-
models: {
|
|
16
|
-
'claude-haiku-4-5': { id: 'claude-haiku-4-5', family: 'claude-haiku', release_date: '2025-10-01' },
|
|
17
|
-
'claude-sonnet-4-5': { id: 'claude-sonnet-4-5', family: 'claude-sonnet', release_date: '2025-09-01' },
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
describe('parseModelRef', () => {
|
|
23
|
-
it('splits provider/model on the first slash', () => {
|
|
24
|
-
expect(parseModelRef('anthropic/claude-haiku-4-5')).toEqual({
|
|
25
|
-
providerID: 'anthropic',
|
|
26
|
-
modelID: 'claude-haiku-4-5',
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('keeps slashes inside the model id', () => {
|
|
31
|
-
expect(parseModelRef('openrouter/google/gemini-2.5-flash')).toEqual({
|
|
32
|
-
providerID: 'openrouter',
|
|
33
|
-
modelID: 'google/gemini-2.5-flash',
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('rejects values without a provider or model part', () => {
|
|
38
|
-
expect(parseModelRef('anthropic/')).toBeNull();
|
|
39
|
-
expect(parseModelRef('/model')).toBeNull();
|
|
40
|
-
expect(parseModelRef('plain')).toBeNull();
|
|
41
|
-
expect(parseModelRef(undefined)).toBeNull();
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('isUsableAuthEntry', () => {
|
|
46
|
-
it('accepts api keys, oauth tokens, and wellknown tokens', () => {
|
|
47
|
-
expect(isUsableAuthEntry({ type: 'api', key: 'sk-x' })).toBe(true);
|
|
48
|
-
expect(isUsableAuthEntry({ type: 'oauth', access: 'a', refresh: 'r', expires: 0 })).toBe(true);
|
|
49
|
-
expect(isUsableAuthEntry({ type: 'wellknown', key: 'k', token: 't' })).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('rejects empty or malformed entries', () => {
|
|
53
|
-
expect(isUsableAuthEntry({ type: 'api', key: '' })).toBe(false);
|
|
54
|
-
expect(isUsableAuthEntry({ type: 'oauth' })).toBe(false);
|
|
55
|
-
expect(isUsableAuthEntry(null)).toBe(false);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('resolveSmallModel', () => {
|
|
60
|
-
it('gives the OpenChamber settings override top priority', () => {
|
|
61
|
-
const result = resolveSmallModel({
|
|
62
|
-
auth: { anthropic: { type: 'api', key: 'sk-x' } },
|
|
63
|
-
catalog,
|
|
64
|
-
settingsSmallModel: 'anthropic/claude-haiku-4-5',
|
|
65
|
-
configSmallModel: 'openai/gpt-4o-mini',
|
|
66
|
-
preferredProviderID: 'anthropic',
|
|
67
|
-
});
|
|
68
|
-
expect(result).toEqual({ providerID: 'anthropic', modelID: 'claude-haiku-4-5', source: 'settings' });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('prefers the configured small_model', () => {
|
|
72
|
-
const result = resolveSmallModel({
|
|
73
|
-
auth: { anthropic: { type: 'api', key: 'sk-x' } },
|
|
74
|
-
catalog,
|
|
75
|
-
configSmallModel: 'openai/gpt-4o-mini',
|
|
76
|
-
});
|
|
77
|
-
expect(result).toEqual({ providerID: 'openai', modelID: 'gpt-4o-mini', source: 'config' });
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('scans authenticated providers by family priority, newest first', () => {
|
|
81
|
-
const result = resolveSmallModel({
|
|
82
|
-
auth: {
|
|
83
|
-
google: { type: 'api', key: 'g-key' },
|
|
84
|
-
anthropic: { type: 'api', key: 'sk-x' },
|
|
85
|
-
},
|
|
86
|
-
catalog,
|
|
87
|
-
configSmallModel: null,
|
|
88
|
-
});
|
|
89
|
-
expect(result).toEqual({ providerID: 'google', modelID: 'gemini-2.5-flash', source: 'family-scan' });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('skips providers without a usable credential', () => {
|
|
93
|
-
const result = resolveSmallModel({
|
|
94
|
-
auth: {
|
|
95
|
-
google: { type: 'api', key: '' },
|
|
96
|
-
anthropic: { type: 'api', key: 'sk-x' },
|
|
97
|
-
},
|
|
98
|
-
catalog,
|
|
99
|
-
configSmallModel: null,
|
|
100
|
-
});
|
|
101
|
-
expect(result).toEqual({ providerID: 'anthropic', modelID: 'claude-haiku-4-5', source: 'family-scan' });
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('falls back to Copilot utility models when only Copilot is logged in', () => {
|
|
105
|
-
const result = resolveSmallModel({
|
|
106
|
-
auth: { 'github-copilot': { type: 'oauth', access: 't', refresh: 't', expires: 0 } },
|
|
107
|
-
catalog,
|
|
108
|
-
configSmallModel: null,
|
|
109
|
-
});
|
|
110
|
-
expect(result?.providerID).toBe('github-copilot');
|
|
111
|
-
expect(result?.source).toBe('copilot-utility');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('returns null when nothing is authenticated', () => {
|
|
115
|
-
expect(resolveSmallModel({ auth: {}, catalog, configSmallModel: null })).toBeNull();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('prefers the session provider over other authenticated providers', () => {
|
|
119
|
-
const result = resolveSmallModel({
|
|
120
|
-
auth: {
|
|
121
|
-
google: { type: 'api', key: 'g-key' },
|
|
122
|
-
anthropic: { type: 'api', key: 'sk-x' },
|
|
123
|
-
},
|
|
124
|
-
catalog,
|
|
125
|
-
configSmallModel: null,
|
|
126
|
-
preferredProviderID: 'anthropic',
|
|
127
|
-
});
|
|
128
|
-
expect(result).toEqual({ providerID: 'anthropic', modelID: 'claude-haiku-4-5', source: 'family-scan' });
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('ignores a preferred provider without a usable login', () => {
|
|
132
|
-
const result = resolveSmallModel({
|
|
133
|
-
auth: { google: { type: 'api', key: 'g-key' } },
|
|
134
|
-
catalog,
|
|
135
|
-
configSmallModel: null,
|
|
136
|
-
preferredProviderID: 'anthropic',
|
|
137
|
-
});
|
|
138
|
-
expect(result).toEqual({ providerID: 'google', modelID: 'gemini-2.5-flash', source: 'family-scan' });
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('never uses a session provider without a login (opencode free models)', () => {
|
|
142
|
-
// Vanilla setups default the picker to opencode/big-pickle with no
|
|
143
|
-
// opencode token — those free models only work through OpenCode itself
|
|
144
|
-
// and must never be called directly, so the session context is ignored.
|
|
145
|
-
const result = resolveSmallModel({
|
|
146
|
-
auth: { openai: { type: 'oauth', access: 'a', refresh: 'r', expires: Date.now() + 60_000 } },
|
|
147
|
-
catalog,
|
|
148
|
-
configSmallModel: null,
|
|
149
|
-
preferredProviderID: 'opencode',
|
|
150
|
-
preferredModelID: 'big-pickle',
|
|
151
|
-
});
|
|
152
|
-
expect(result).toEqual({ providerID: 'openai', modelID: 'gpt-5.4-mini', source: 'codex-small' });
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('resolves nothing on a vanilla setup with no logins at all', () => {
|
|
156
|
-
const result = resolveSmallModel({
|
|
157
|
-
auth: {},
|
|
158
|
-
catalog,
|
|
159
|
-
configSmallModel: null,
|
|
160
|
-
preferredProviderID: 'opencode',
|
|
161
|
-
preferredModelID: 'big-pickle',
|
|
162
|
-
});
|
|
163
|
-
expect(result).toBeNull();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('falls back to the session model instead of scanning other providers', () => {
|
|
167
|
-
const result = resolveSmallModel({
|
|
168
|
-
auth: {
|
|
169
|
-
'opencode-go': { type: 'api', key: 'oc-key' },
|
|
170
|
-
openai: { type: 'oauth', access: 'a', refresh: 'r', expires: Date.now() + 60_000 },
|
|
171
|
-
},
|
|
172
|
-
catalog: {
|
|
173
|
-
'opencode-go': {
|
|
174
|
-
id: 'opencode-go',
|
|
175
|
-
models: {
|
|
176
|
-
'deepseek-v4-flash': { id: 'deepseek-v4-flash', family: 'deepseek-flash', release_date: '2026-01-01' },
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
configSmallModel: null,
|
|
181
|
-
preferredProviderID: 'opencode-go',
|
|
182
|
-
preferredModelID: 'deepseek-v4-flash',
|
|
183
|
-
});
|
|
184
|
-
expect(result).toEqual({ providerID: 'opencode-go', modelID: 'deepseek-v4-flash', source: 'session-model' });
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('falls back to the session model itself when nothing resolves', () => {
|
|
188
|
-
const result = resolveSmallModel({
|
|
189
|
-
auth: { mistral: { type: 'api', key: 'm-key' } },
|
|
190
|
-
catalog,
|
|
191
|
-
configSmallModel: null,
|
|
192
|
-
preferredProviderID: 'mistral',
|
|
193
|
-
preferredModelID: 'mistral-large-latest',
|
|
194
|
-
});
|
|
195
|
-
expect(result).toEqual({ providerID: 'mistral', modelID: 'mistral-large-latest', source: 'session-model' });
|
|
196
|
-
});
|
|
197
|
-
});
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
// Provider state that exists only inside the running OpenCode process.
|
|
2
|
-
//
|
|
3
|
-
// A plugin registers its provider from the `config` hook and supplies the
|
|
4
|
-
// credential from its `auth` loader, both at startup. Neither ends up in
|
|
5
|
-
// `opencode.json` or `auth.json`, so a server that only reads files sees
|
|
6
|
-
// nothing — which is why plugin-backed models used to fail here with
|
|
7
|
-
// "has no known API base URL" while working fine in chat (#2666).
|
|
8
|
-
//
|
|
9
|
-
// `GET /provider` is where that state becomes visible. It reports, per
|
|
10
|
-
// provider, the resolved `options.baseURL` and `options.apiKey`, and per model
|
|
11
|
-
// the wire adapter (`api.npm`) and endpoint (`api.url`). The provider-level
|
|
12
|
-
// endpoint remains only as a fallback when the selected model has no endpoint.
|
|
13
|
-
//
|
|
14
|
-
// What it does NOT report is `options.fetch`. OpenCode strips functions from
|
|
15
|
-
// the response, and a plugin is free to put its whole protocol in there:
|
|
16
|
-
// rewriting the path, signing the request, translating the payload. Such a
|
|
17
|
-
// provider advertises a perfectly ordinary base URL that answers nothing we
|
|
18
|
-
// know how to ask, and no field distinguishes the two.
|
|
19
|
-
//
|
|
20
|
-
// Asking the endpoint (`GET /models`) looked like the way to tell them apart,
|
|
21
|
-
// and it does answer correctly for that case — but measured against the 166
|
|
22
|
-
// providers in the models.dev catalog it also denies six that work fine and
|
|
23
|
-
// simply have no `/models` route. A provider that vanishes from the picker
|
|
24
|
-
// explains nothing; one that fails on use says why. So this module reports
|
|
25
|
-
// what it knows and leaves the verdict to the call itself.
|
|
26
|
-
|
|
27
|
-
const SNAPSHOT_TTL_MS = 30_000;
|
|
28
|
-
const SNAPSHOT_TIMEOUT_MS = 5_000;
|
|
29
|
-
|
|
30
|
-
// opencode zen hands out this sentinel instead of a key when the user has no
|
|
31
|
-
// zen login, and trims its catalog to the free models. Those run on OpenCode's
|
|
32
|
-
// own subsidised infrastructure and are meant to be reached through OpenCode,
|
|
33
|
-
// not by us. Treating the sentinel as a credential would do exactly that, so
|
|
34
|
-
// it is never accepted as one.
|
|
35
|
-
export const ZEN_ANONYMOUS_API_KEY = 'public';
|
|
36
|
-
|
|
37
|
-
let connection = null;
|
|
38
|
-
let snapshot = null;
|
|
39
|
-
let snapshotAt = 0;
|
|
40
|
-
let inflight = null;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Wires this module to the running OpenCode instance. Called once at server
|
|
44
|
-
* startup; pass `null` to detach. Until it is wired every lookup answers
|
|
45
|
-
* "nothing known", which leaves the file-based resolution unchanged.
|
|
46
|
-
*/
|
|
47
|
-
export function configureOpenCodeRuntimeProviders(next) {
|
|
48
|
-
connection = next ?? null;
|
|
49
|
-
resetOpenCodeRuntimeProviders();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Drops every cached answer. OpenCode restarts reload plugins, which can
|
|
54
|
-
* change ports, keys and the provider list itself.
|
|
55
|
-
*/
|
|
56
|
-
export function resetOpenCodeRuntimeProviders() {
|
|
57
|
-
snapshot = null;
|
|
58
|
-
snapshotAt = 0;
|
|
59
|
-
inflight = null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* The boundary. Everything the `/provider` payload claims is checked here, so
|
|
64
|
-
* the rest of this module and its callers work with settled values:
|
|
65
|
-
* a credential we may use, provider and model endpoints, and whether the
|
|
66
|
-
* provider is the anonymous zen case.
|
|
67
|
-
*
|
|
68
|
-
* The credential deliberately prefers `options.apiKey` over the `key` field:
|
|
69
|
-
* for a plugin provider the former is what its auth loader produced and what
|
|
70
|
-
* OpenCode itself sends, while `key` only carries env/auth.json values this
|
|
71
|
-
* server can already read from disk.
|
|
72
|
-
*/
|
|
73
|
-
function parseProviderListing(payload) {
|
|
74
|
-
const providers = new Map();
|
|
75
|
-
const connected = new Set();
|
|
76
|
-
if (!payload || typeof payload !== 'object') return { providers, connected };
|
|
77
|
-
|
|
78
|
-
const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
|
|
79
|
-
const record = (value) => (value && typeof value === 'object' ? value : {});
|
|
80
|
-
const endpoint = (value) => text(value)?.replace(/\/+$/, '') ?? null;
|
|
81
|
-
|
|
82
|
-
for (const raw of Array.isArray(payload.all) ? payload.all : []) {
|
|
83
|
-
const id = text(record(raw).id);
|
|
84
|
-
if (!id) continue;
|
|
85
|
-
const options = record(record(raw).options);
|
|
86
|
-
const models = new Map();
|
|
87
|
-
for (const [modelID, rawModel] of Object.entries(record(record(raw).models))) {
|
|
88
|
-
const model = record(rawModel);
|
|
89
|
-
const api = record(model.api);
|
|
90
|
-
const modelURL = endpoint(api.url);
|
|
91
|
-
const modelNpm = text(api.npm);
|
|
92
|
-
models.set(modelID, { api: { url: modelURL, npm: modelNpm } });
|
|
93
|
-
}
|
|
94
|
-
const firstModel = models.values().next().value;
|
|
95
|
-
const declaredKey = text(options.apiKey);
|
|
96
|
-
providers.set(id, {
|
|
97
|
-
id,
|
|
98
|
-
source: text(record(raw).source),
|
|
99
|
-
apiKey: declaredKey === ZEN_ANONYMOUS_API_KEY ? null : (declaredKey ?? text(record(raw).key)),
|
|
100
|
-
baseURL: endpoint(options.baseURL) ?? firstModel?.api?.url ?? null,
|
|
101
|
-
models,
|
|
102
|
-
// True only for the zen-without-login case: a provider that is present
|
|
103
|
-
// and usable through OpenCode, but that we must not call ourselves.
|
|
104
|
-
anonymousZen: declaredKey === ZEN_ANONYMOUS_API_KEY,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Providers OpenCode considers usable right now. A provider can be present
|
|
109
|
-
// in `all` (it is in the catalog) without any credential behind it.
|
|
110
|
-
for (const raw of Array.isArray(payload.connected) ? payload.connected : []) {
|
|
111
|
-
const id = text(raw);
|
|
112
|
-
if (id) connected.add(id);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return { providers, connected };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const fetchSnapshot = async () => {
|
|
119
|
-
const response = await fetch(connection.buildOpenCodeUrl('/provider', ''), {
|
|
120
|
-
headers: { Accept: 'application/json', ...connection.getOpenCodeAuthHeaders() },
|
|
121
|
-
signal: AbortSignal.timeout(SNAPSHOT_TIMEOUT_MS),
|
|
122
|
-
});
|
|
123
|
-
if (!response.ok) {
|
|
124
|
-
throw new Error(`OpenCode provider listing failed with ${response.status}`);
|
|
125
|
-
}
|
|
126
|
-
return parseProviderListing(await response.json());
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* The current runtime provider snapshot, or `null` when OpenCode cannot be
|
|
131
|
-
* reached.
|
|
132
|
-
*
|
|
133
|
-
* `null` means "unknown", never "no providers": callers must fall back to
|
|
134
|
-
* their file-based resolution rather than treat an unreachable OpenCode as an
|
|
135
|
-
* empty provider list.
|
|
136
|
-
*/
|
|
137
|
-
export async function getRuntimeProviderSnapshot() {
|
|
138
|
-
if (!connection) return null;
|
|
139
|
-
if (snapshot && Date.now() - snapshotAt < SNAPSHOT_TTL_MS) return snapshot;
|
|
140
|
-
if (!inflight) {
|
|
141
|
-
inflight = fetchSnapshot().finally(() => {
|
|
142
|
-
inflight = null;
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
try {
|
|
146
|
-
snapshot = await inflight;
|
|
147
|
-
snapshotAt = Date.now();
|
|
148
|
-
return snapshot;
|
|
149
|
-
} catch {
|
|
150
|
-
// Keep serving the previous snapshot when there is one: a momentarily
|
|
151
|
-
// unreachable OpenCode should not retract providers that were resolving a
|
|
152
|
-
// second ago.
|
|
153
|
-
return snapshot;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Runtime credential and endpoint for one provider, or `null` when OpenCode
|
|
159
|
-
* knows nothing about it.
|
|
160
|
-
*/
|
|
161
|
-
export async function getRuntimeProvider(providerID) {
|
|
162
|
-
const current = await getRuntimeProviderSnapshot();
|
|
163
|
-
return current?.providers.get(providerID) ?? null;
|
|
164
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ZEN_ANONYMOUS_API_KEY,
|
|
5
|
-
configureOpenCodeRuntimeProviders,
|
|
6
|
-
getRuntimeProvider,
|
|
7
|
-
getRuntimeProviderSnapshot,
|
|
8
|
-
resetOpenCodeRuntimeProviders,
|
|
9
|
-
} from './runtime-providers.js';
|
|
10
|
-
|
|
11
|
-
const providerPayload = (overrides = {}) => ({
|
|
12
|
-
all: [
|
|
13
|
-
{
|
|
14
|
-
id: 'llmapi',
|
|
15
|
-
source: 'config',
|
|
16
|
-
options: { apiKey: 'plugin-key', baseURL: 'https://api.llmapi.ai/v1/' },
|
|
17
|
-
models: {
|
|
18
|
-
'claude-opus-4-8': { api: { id: 'claude-opus-4-8', url: '', npm: '@ai-sdk/anthropic' } },
|
|
19
|
-
'gpt-5.6-luna': { api: { id: 'gpt-5.6-luna', url: 'https://api.llmapi.ai/v1', npm: '@ai-sdk/openai' } },
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
id: 'opencode',
|
|
24
|
-
source: 'custom',
|
|
25
|
-
options: { apiKey: ZEN_ANONYMOUS_API_KEY },
|
|
26
|
-
models: { 'free-model': { api: { id: 'free-model', url: 'https://opencode.ai/zen/v1', npm: '@ai-sdk/openai-compatible' } } },
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
id: 'zai-coding-plan',
|
|
30
|
-
source: 'api',
|
|
31
|
-
key: 'auth-json-key',
|
|
32
|
-
options: {},
|
|
33
|
-
models: { 'glm-5': { api: { id: 'glm-5', url: 'https://api.z.ai/api/coding/paas/v4', npm: '@ai-sdk/openai-compatible' } } },
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
connected: ['llmapi', 'opencode', 'zai-coding-plan'],
|
|
37
|
-
...overrides,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('OpenCode runtime provider snapshot', () => {
|
|
41
|
-
let fetchMock;
|
|
42
|
-
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
fetchMock = vi.fn(async () => new Response(JSON.stringify(providerPayload()), {
|
|
45
|
-
status: 200,
|
|
46
|
-
headers: { 'content-type': 'application/json' },
|
|
47
|
-
}));
|
|
48
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
49
|
-
configureOpenCodeRuntimeProviders({
|
|
50
|
-
buildOpenCodeUrl: (pathname) => `http://127.0.0.1:4096${pathname}`,
|
|
51
|
-
getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic test' }),
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
afterEach(() => {
|
|
56
|
-
configureOpenCodeRuntimeProviders(null);
|
|
57
|
-
resetOpenCodeRuntimeProviders();
|
|
58
|
-
vi.unstubAllGlobals();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('reports the credential and endpoint a plugin registered at runtime', async () => {
|
|
62
|
-
const provider = await getRuntimeProvider('llmapi');
|
|
63
|
-
|
|
64
|
-
expect(provider).toMatchObject({ apiKey: 'plugin-key', baseURL: 'https://api.llmapi.ai/v1' });
|
|
65
|
-
expect(provider.models.get('gpt-5.6-luna')).toEqual({
|
|
66
|
-
api: { url: 'https://api.llmapi.ai/v1', npm: '@ai-sdk/openai' },
|
|
67
|
-
});
|
|
68
|
-
expect(fetchMock.mock.calls[0][0]).toBe('http://127.0.0.1:4096/provider');
|
|
69
|
-
expect(fetchMock.mock.calls[0][1].headers).toMatchObject({ Authorization: 'Basic test' });
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('refuses the zen sentinel as a credential', async () => {
|
|
73
|
-
const provider = await getRuntimeProvider('opencode');
|
|
74
|
-
|
|
75
|
-
expect(provider.apiKey).toBeNull();
|
|
76
|
-
expect(provider.anonymousZen).toBe(true);
|
|
77
|
-
// The endpoint is still reported; only the credential is withheld.
|
|
78
|
-
expect(provider.baseURL).toBe('https://opencode.ai/zen/v1');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('falls back to the model endpoint when the provider carries no baseURL', async () => {
|
|
82
|
-
expect((await getRuntimeProvider('zai-coding-plan')).baseURL).toBe('https://api.z.ai/api/coding/paas/v4');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('serves one snapshot to concurrent callers instead of refetching', async () => {
|
|
86
|
-
await Promise.all([getRuntimeProvider('llmapi'), getRuntimeProvider('opencode'), getRuntimeProvider('zai-coding-plan')]);
|
|
87
|
-
|
|
88
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('answers "unknown" rather than "no providers" when OpenCode is unreachable', async () => {
|
|
92
|
-
resetOpenCodeRuntimeProviders();
|
|
93
|
-
fetchMock.mockRejectedValue(new Error('connection refused'));
|
|
94
|
-
|
|
95
|
-
expect(await getRuntimeProviderSnapshot()).toBeNull();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('keeps the previous snapshot when a later refresh fails', async () => {
|
|
99
|
-
await getRuntimeProviderSnapshot();
|
|
100
|
-
fetchMock.mockRejectedValue(new Error('connection refused'));
|
|
101
|
-
|
|
102
|
-
// Past the snapshot TTL, so the next read genuinely attempts a refresh.
|
|
103
|
-
vi.useFakeTimers();
|
|
104
|
-
vi.setSystemTime(Date.now() + 60_000);
|
|
105
|
-
const refreshed = await getRuntimeProviderSnapshot();
|
|
106
|
-
vi.useRealTimers();
|
|
107
|
-
|
|
108
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
109
|
-
expect(refreshed.providers.has('llmapi')).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('stays on file-based resolution until it is configured', async () => {
|
|
113
|
-
configureOpenCodeRuntimeProviders(null);
|
|
114
|
-
|
|
115
|
-
expect(await getRuntimeProvider('llmapi')).toBeNull();
|
|
116
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
117
|
-
});
|
|
118
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Managed System Prompt Optimizer
|
|
2
|
-
|
|
3
|
-
## Purpose
|
|
4
|
-
|
|
5
|
-
This module injects an opt-in OpenCode plugin only when OpenChamber launches
|
|
6
|
-
and owns the OpenCode process and `optimizeSystemPrompt` is enabled. The plugin
|
|
7
|
-
replaces OpenCode's built-in behavioral/provider prompt with a short identity
|
|
8
|
-
while preserving the environment, project instructions, MCP instructions,
|
|
9
|
-
skills, conversation history, and separately supplied tools.
|
|
10
|
-
|
|
11
|
-
## Runtime flow
|
|
12
|
-
|
|
13
|
-
1. Settings persist `optimizeSystemPrompt` in OpenChamber's `settings.json`.
|
|
14
|
-
2. The setting is applied when managed OpenCode restarts.
|
|
15
|
-
3. The runtime materializes the plugin under
|
|
16
|
-
`<openchamber-data-dir>/system-prompt/` and appends its `file://` URL to
|
|
17
|
-
`OPENCODE_CONFIG_CONTENT` without replacing existing plugin entries.
|
|
18
|
-
4. The plugin tracks the selected agent through `chat.message`. The transform
|
|
19
|
-
runs only for sessions using the built-in `build` or `plan` agent.
|
|
20
|
-
5. The transform locates OpenCode's environment boundary and removes only the
|
|
21
|
-
preceding text. If the boundary is absent, it leaves the prompt unchanged.
|
|
22
|
-
|
|
23
|
-
## Limitations
|
|
24
|
-
|
|
25
|
-
OpenCode exposes the assembled prompt rather than structured sections. A custom
|
|
26
|
-
prompt configured by overriding the `build` or `plan` agent occupies the same
|
|
27
|
-
prefix as the built-in provider prompt, so the optimizer also removes that
|
|
28
|
-
override. Other agents are never transformed. The setting is off by default.
|
|
29
|
-
|
|
30
|
-
Plan-mode restrictions and build-mode transitions are not part of the removed
|
|
31
|
-
prefix. OpenCode injects those as synthetic message reminders after system
|
|
32
|
-
prompt transformation and separately enforces plan restrictions through tool
|
|
33
|
-
permissions.
|
|
34
|
-
|
|
35
|
-
The plugin is not injected for external OpenCode servers or VS Code's separate
|
|
36
|
-
OpenCode lifecycle.
|