@vantran-se/openchamber-web 1.24.3 → 2.0.1
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-BjJHrzmU.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
- package/dist/assets/DiffView-CjxUovbM.js +4 -0
- package/dist/assets/FilesView-DFWD0RQR.js +17 -0
- package/dist/assets/{GitView-CDOJy5Ay.js → GitView-BWa7e5vw.js} +4 -4
- package/dist/assets/{GuestAttachDialog-D3S0Rjic.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
- package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
- package/dist/assets/{JsonTreeView-D0C9nU7K.js → JsonTreeView-3sGwqexh.js} +1 -1
- package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
- package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
- package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
- package/dist/assets/{PatchDiff-DnEhjXcC.js → PatchDiff-DS-IMU1j.js} +1 -1
- package/dist/assets/{PierreDiffViewer-DyxZVVVR.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
- package/dist/assets/PlanView-BCk_VobP.js +5 -0
- package/dist/assets/PluginPane-BPP1dbck.js +3 -0
- package/dist/assets/SettingsView-kMmrpn43.js +29 -0
- package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
- package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
- package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-0pyr_mHY.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
- package/dist/assets/{WalkthroughView-yNj4nOP5.js → WalkthroughView-lap5btCF.js} +2 -2
- package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
- package/dist/assets/{commitAndPush-DvJKa3Ks.js → commitAndPush-CwGJfJG-.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DxTBMuu-.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
- package/dist/assets/de-CndEg67T.js +10 -0
- package/dist/assets/es-DglvPlaC.js +16 -0
- package/dist/assets/fr-DejPCpbS.js +14 -0
- package/dist/assets/index-C2r64QfD.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-D7FkBMP6.js → isDiffPlainText-D47a6hMP.js} +1 -1
- package/dist/assets/{isFilePlainText-CL2vsrIZ.js → isFilePlainText-B-mGgeKl.js} +1 -1
- package/dist/assets/ja-CVpQawCd.js +16 -0
- package/dist/assets/ko-Bw446h9j.js +16 -0
- package/dist/assets/main-Bz4WWXbr.js +2 -0
- package/dist/assets/main-oreqDsOV.js +896 -0
- package/dist/assets/{markdownCore-Dh7WEsdM.js → markdownCore-D2e5sp9y.js} +4 -4
- package/dist/assets/miniChat-D1v2gsBI.js +2 -0
- package/dist/assets/mobile-DBj3lCf2.js +2 -0
- package/dist/assets/{patchFileDiff-Bsba8KFp.js → patchFileDiff-BpeUOV91.js} +3 -3
- package/dist/assets/pl-BYNlPdhm.js +16 -0
- package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
- package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
- package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
- package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
- package/dist/assets/{shikiHighlight-8_3csmr6.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-CHo2fCgW.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
- package/dist/assets/tr-DzmbxLy-.js +7 -0
- package/dist/assets/uk-C7vQnwro.js +16 -0
- package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
- package/dist/assets/{useInlineCommentController-DkQ_gqpn.js → useInlineCommentController-CuUuakrt.js} +2 -2
- package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-CdzUmUyZ.js → vendor-codemirror-language-data-CQa7_1cr.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-B8IcVQAA.js → web-components-B6tKQyJN.js} +2 -2
- package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
- package/dist/assets/zh-TW-DtII5bDb.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-COVQhWGd.js +0 -4
- package/dist/assets/FilesView-B1V5JxFX.js +0 -13
- package/dist/assets/LinearIssuesView-D7FKg1XJ.js +0 -1
- package/dist/assets/MarkdownImageGallery-BrFjAVEf.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DWepTafh.js +0 -12
- package/dist/assets/OnboardingScreen-Dyxxiqdt.js +0 -1
- package/dist/assets/PlanView-LW2z_EAP.js +0 -5
- package/dist/assets/PluginPane-B4tKno2h.js +0 -3
- package/dist/assets/SettingsView-DEaMulN9.js +0 -29
- package/dist/assets/SettingsWindow-FLDJXOgQ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +0 -1
- package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CkR4msKP.js +0 -8
- package/dist/assets/appThemeRegistry-BuUKhbYQ.js +0 -1
- package/dist/assets/de-DgxgFyGG.js +0 -10
- package/dist/assets/es-BFsZj5F_.js +0 -16
- package/dist/assets/fr-jdZ5KKQi.js +0 -14
- package/dist/assets/index-CKHjPKbq.js +0 -1
- package/dist/assets/index-Cbci4i1W.js +0 -40
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/ja-C5kKrhO4.js +0 -16
- package/dist/assets/ko-DXDTT_0O.js +0 -16
- package/dist/assets/main-CZALtVek.js +0 -896
- package/dist/assets/main-CgXMBNvG.js +0 -2
- package/dist/assets/miniChat-CwTmDixi.js +0 -2
- package/dist/assets/mobile-CLf2vCtO.js +0 -2
- package/dist/assets/pl-D-7x4Y8Q.js +0 -16
- package/dist/assets/pt-BR-CSBZrHa8.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +0 -2
- package/dist/assets/renderMobileApp-V4hYd32H.js +0 -25
- package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +0 -25
- package/dist/assets/scheduledTasksApi-DzjbLd8P.js +0 -1
- package/dist/assets/tr-Dr-oJ22L.js +0 -7
- package/dist/assets/uk-9Nx83UcI.js +0 -16
- package/dist/assets/useAppFontEffects-khZXpFL_.js +0 -7333
- package/dist/assets/useRootScrollLock-DdvHf9Pt.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-Bxy_Hj3r.js +0 -16
- package/dist/assets/zh-TW-0BB4gpHM.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
|
@@ -45,19 +45,21 @@ describe('opencode plugins data layer', () => {
|
|
|
45
45
|
delete process.env.OPENCODE_CONFIG;
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
test('parses
|
|
48
|
+
test('parses v1 tuples and v2 objects into the same entry', () => {
|
|
49
49
|
expect(plugins.parsePluginRaw('foo')).toEqual({ spec: 'foo' });
|
|
50
50
|
expect(plugins.parsePluginRaw('foo@1.0.0')).toEqual({ spec: 'foo@1.0.0' });
|
|
51
51
|
expect(plugins.parsePluginRaw(['foo', { a: 1 }])).toEqual({ spec: 'foo', options: { a: 1 } });
|
|
52
|
-
expect(plugins.parsePluginRaw(['foo', {}])).toEqual({ spec: 'foo'
|
|
52
|
+
expect(plugins.parsePluginRaw(['foo', {}])).toEqual({ spec: 'foo' });
|
|
53
|
+
expect(plugins.parsePluginRaw({ package: 'foo', options: { a: 1 } })).toEqual({ spec: 'foo', options: { a: 1 } });
|
|
54
|
+
expect(plugins.parsePluginRaw({ package: 'foo' })).toEqual({ spec: 'foo' });
|
|
53
55
|
expect(() => plugins.parsePluginRaw(123)).toThrow('Plugin spec');
|
|
54
56
|
});
|
|
55
57
|
|
|
56
|
-
test('serializes plugin entries', () => {
|
|
58
|
+
test('serializes plugin entries in v2 shape, never a tuple', () => {
|
|
57
59
|
expect(plugins.serializePluginEntry({ spec: 'foo' })).toBe('foo');
|
|
58
60
|
expect(plugins.serializePluginEntry({ spec: 'foo', options: undefined })).toBe('foo');
|
|
59
61
|
expect(plugins.serializePluginEntry({ spec: 'foo', options: {} })).toBe('foo');
|
|
60
|
-
expect(plugins.serializePluginEntry({ spec: 'foo', options: { a: 1 } })).toEqual(
|
|
62
|
+
expect(plugins.serializePluginEntry({ spec: 'foo', options: { a: 1 } })).toEqual({ package: 'foo', options: { a: 1 } });
|
|
61
63
|
});
|
|
62
64
|
|
|
63
65
|
test('rejects invalid specs and file names', () => {
|
|
@@ -69,11 +71,12 @@ describe('opencode plugins data layer', () => {
|
|
|
69
71
|
expect(() => plugins.writePluginDirFile({ fileName: 'foo.txt', content: '', scope: 'project' }, projectDir)).toThrow('Plugin file name');
|
|
70
72
|
});
|
|
71
73
|
|
|
72
|
-
test('creates string and
|
|
74
|
+
test('creates string and object entries with duplicate rejection', () => {
|
|
73
75
|
plugins.createPluginEntry({ spec: 'plain-plugin', scope: 'user' }, projectDir);
|
|
74
76
|
plugins.createPluginEntry({ spec: 'tuple-plugin', options: { apiKey: 'x' }, scope: 'user' }, projectDir);
|
|
75
77
|
|
|
76
|
-
expect(readJson(userConfigPath).
|
|
78
|
+
expect(readJson(userConfigPath).plugins).toEqual(['plain-plugin', { package: 'tuple-plugin', options: { apiKey: 'x' } }]);
|
|
79
|
+
expect(readJson(userConfigPath).plugin).toBeUndefined();
|
|
77
80
|
expect(() => plugins.createPluginEntry({ spec: 'plain-plugin', scope: 'user' }, projectDir)).toThrow('already exists');
|
|
78
81
|
expect(thrownBy(() => plugins.createPluginEntry({ spec: 'plain-plugin', scope: 'user' }, projectDir))).toHaveProperty('code', 'ENTRY_EXISTS');
|
|
79
82
|
});
|
|
@@ -82,8 +85,8 @@ describe('opencode plugins data layer', () => {
|
|
|
82
85
|
plugins.createPluginEntry({ spec: 'user-plugin', scope: 'user' }, projectDir);
|
|
83
86
|
plugins.createPluginEntry({ spec: 'project-plugin', scope: 'project' }, projectDir);
|
|
84
87
|
|
|
85
|
-
expect(readJson(userConfigPath).
|
|
86
|
-
expect(readJson(path.join(projectDir, '.opencode', 'opencode.json')).
|
|
88
|
+
expect(readJson(userConfigPath).plugins).toEqual(['user-plugin']);
|
|
89
|
+
expect(readJson(path.join(projectDir, '.opencode', 'opencode.json')).plugins).toEqual(['project-plugin']);
|
|
87
90
|
});
|
|
88
91
|
|
|
89
92
|
test('re-resolves custom config env between calls', () => {
|
|
@@ -98,24 +101,34 @@ describe('opencode plugins data layer', () => {
|
|
|
98
101
|
plugins.createPluginEntry({ spec: 'second-plugin', scope: 'user' }, projectDir);
|
|
99
102
|
plugins.writePluginDirFile({ fileName: 'second.js', content: 'two', scope: 'user' }, projectDir);
|
|
100
103
|
|
|
101
|
-
expect(readJson(firstConfigPath).
|
|
102
|
-
expect(readJson(secondConfigPath).
|
|
104
|
+
expect(readJson(firstConfigPath).plugins).toEqual(['first-plugin']);
|
|
105
|
+
expect(readJson(secondConfigPath).plugins).toEqual(['second-plugin']);
|
|
103
106
|
expect(fs.existsSync(path.join(path.dirname(firstConfigPath), 'plugins', 'first.js'))).toBe(true);
|
|
104
107
|
expect(fs.existsSync(path.join(path.dirname(secondConfigPath), 'plugins', 'second.js'))).toBe(true);
|
|
105
108
|
});
|
|
106
109
|
|
|
107
|
-
test('
|
|
110
|
+
test('rewrites a v1 plugin array into v2 plugins when one entry is updated', () => {
|
|
108
111
|
writeJson(userConfigPath, { plugin: ['first', ['second', { a: 1 }], 'third'] });
|
|
109
112
|
|
|
110
113
|
plugins.updatePluginEntry(plugins.encodePluginId('config', 'user:second'), { spec: 'second-new', options: {} }, projectDir);
|
|
111
|
-
|
|
114
|
+
const migrated = readJson(userConfigPath);
|
|
115
|
+
expect(migrated.plugin).toBeUndefined();
|
|
116
|
+
expect(migrated.plugins).toEqual(['first', 'second-new', 'third']);
|
|
112
117
|
|
|
113
118
|
plugins.updatePluginEntry(plugins.encodePluginId('config', 'user:first'), { spec: 'first-new', options: { b: 2 } }, projectDir);
|
|
114
|
-
expect(readJson(userConfigPath).
|
|
119
|
+
expect(readJson(userConfigPath).plugins).toEqual([{ package: 'first-new', options: { b: 2 } }, 'second-new', 'third']);
|
|
115
120
|
});
|
|
116
121
|
|
|
117
|
-
test('
|
|
118
|
-
writeJson(userConfigPath, {
|
|
122
|
+
test('reads a v2 plugins array and updates it in place', () => {
|
|
123
|
+
writeJson(userConfigPath, { plugins: ['alpha', { package: 'beta', options: { a: 1 } }] });
|
|
124
|
+
|
|
125
|
+
expect(plugins.listPluginEntries(projectDir).map((entry) => entry.spec)).toEqual(['alpha', 'beta']);
|
|
126
|
+
plugins.updatePluginEntry(plugins.encodePluginId('config', 'user:beta'), { options: { a: 2 } }, projectDir);
|
|
127
|
+
expect(readJson(userConfigPath).plugins).toEqual(['alpha', { package: 'beta', options: { a: 2 } }]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('deletes entries and prunes the empty plugins key', () => {
|
|
131
|
+
writeJson(userConfigPath, { plugins: ['only'] });
|
|
119
132
|
|
|
120
133
|
plugins.deletePluginEntry(plugins.encodePluginId('config', 'user:only'), projectDir);
|
|
121
134
|
expect(readJson(userConfigPath)).toEqual({});
|
|
@@ -5,6 +5,15 @@ import {
|
|
|
5
5
|
getConfigForPath,
|
|
6
6
|
writeConfig,
|
|
7
7
|
} from './shared.js';
|
|
8
|
+
import {
|
|
9
|
+
readSectionEntry,
|
|
10
|
+
writeSectionEntry,
|
|
11
|
+
deleteSectionEntry,
|
|
12
|
+
toModelVariants,
|
|
13
|
+
toProviderEntity,
|
|
14
|
+
toProviderPackage,
|
|
15
|
+
toNpmPackage,
|
|
16
|
+
} from './config-v2.js';
|
|
8
17
|
|
|
9
18
|
const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-_]*$/;
|
|
10
19
|
const BASE_URL_PATTERN = /^https?:\/\//;
|
|
@@ -15,34 +24,25 @@ const CUSTOM_PROVIDER_NPM_PACKAGES = new Set([
|
|
|
15
24
|
'@ai-sdk/anthropic',
|
|
16
25
|
]);
|
|
17
26
|
|
|
27
|
+
// OpenCode 2 keeps providers under `providers` with `package: "aisdk:<npm>"`,
|
|
28
|
+
// `settings.baseURL`, and `models.<id>.modelID`. The v1 `provider` map with
|
|
29
|
+
// `npm`/`api`/`options` is still decoded, so reads accept it; every write is v2.
|
|
30
|
+
|
|
31
|
+
function providerExistsIn(config, providerId) {
|
|
32
|
+
return readSectionEntry(config, 'providers', providerId).value !== undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
function getProviderSources(providerId, workingDirectory) {
|
|
19
36
|
const layers = readConfigLayers(workingDirectory);
|
|
20
37
|
const { userConfig, projectConfig, customConfig, paths } = layers;
|
|
21
38
|
|
|
22
|
-
const customProviders = isPlainObject(customConfig?.provider) ? customConfig.provider : {};
|
|
23
|
-
const customProvidersAlias = isPlainObject(customConfig?.providers) ? customConfig.providers : {};
|
|
24
|
-
const projectProviders = isPlainObject(projectConfig?.provider) ? projectConfig.provider : {};
|
|
25
|
-
const projectProvidersAlias = isPlainObject(projectConfig?.providers) ? projectConfig.providers : {};
|
|
26
|
-
const userProviders = isPlainObject(userConfig?.provider) ? userConfig.provider : {};
|
|
27
|
-
const userProvidersAlias = isPlainObject(userConfig?.providers) ? userConfig.providers : {};
|
|
28
|
-
|
|
29
|
-
const customExists =
|
|
30
|
-
Object.prototype.hasOwnProperty.call(customProviders, providerId) ||
|
|
31
|
-
Object.prototype.hasOwnProperty.call(customProvidersAlias, providerId);
|
|
32
|
-
const projectExists =
|
|
33
|
-
Object.prototype.hasOwnProperty.call(projectProviders, providerId) ||
|
|
34
|
-
Object.prototype.hasOwnProperty.call(projectProvidersAlias, providerId);
|
|
35
|
-
const userExists =
|
|
36
|
-
Object.prototype.hasOwnProperty.call(userProviders, providerId) ||
|
|
37
|
-
Object.prototype.hasOwnProperty.call(userProvidersAlias, providerId);
|
|
38
|
-
|
|
39
39
|
return {
|
|
40
40
|
sources: {
|
|
41
41
|
auth: { exists: false },
|
|
42
|
-
user: { exists:
|
|
43
|
-
project: { exists:
|
|
44
|
-
custom: { exists:
|
|
45
|
-
}
|
|
42
|
+
user: { exists: providerExistsIn(userConfig, providerId), path: paths.userPath },
|
|
43
|
+
project: { exists: providerExistsIn(projectConfig, providerId), path: paths.projectPath || null },
|
|
44
|
+
custom: { exists: providerExistsIn(customConfig, providerId), path: paths.customPath },
|
|
45
|
+
},
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -50,6 +50,9 @@ function getProviderSources(providerId, workingDirectory) {
|
|
|
50
50
|
* Validate a custom provider config payload before persistence.
|
|
51
51
|
* Returns { ok: true, value } or { ok: false, error }.
|
|
52
52
|
*
|
|
53
|
+
* Accepts the v2 spelling (`package`, `settings.baseURL`) and the v1 spelling
|
|
54
|
+
* (`npm`, `options.baseURL`); the normalized value is always v2.
|
|
55
|
+
*
|
|
53
56
|
* Credentials: either config.env contains a variable name, or hasStoredAuth is true
|
|
54
57
|
* (auth.json already has a key — typically after auth.set, or when editing).
|
|
55
58
|
*/
|
|
@@ -67,17 +70,19 @@ function validateCustomProviderConfig(providerId, config, options = {}) {
|
|
|
67
70
|
return { ok: false, error: 'Provider name is required' };
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
const npm =
|
|
73
|
+
const npm = toNpmPackage(config.package ?? config.npm) || OPENAI_COMPATIBLE_NPM;
|
|
71
74
|
if (!CUSTOM_PROVIDER_NPM_PACKAGES.has(npm)) {
|
|
72
75
|
return { ok: false, error: 'Custom providers must use @ai-sdk/openai-compatible, @ai-sdk/openai, or @ai-sdk/anthropic' };
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
const settingsBlock = isPlainObject(config.settings)
|
|
79
|
+
? config.settings
|
|
80
|
+
: (isPlainObject(config.options) ? config.options : null);
|
|
81
|
+
if (!settingsBlock) {
|
|
82
|
+
return { ok: false, error: 'Provider settings are required' };
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
const baseURL = typeof
|
|
85
|
+
const baseURL = typeof settingsBlock.baseURL === 'string' ? settingsBlock.baseURL.trim() : '';
|
|
81
86
|
if (!baseURL) {
|
|
82
87
|
return { ok: false, error: 'Base URL is required' };
|
|
83
88
|
}
|
|
@@ -103,15 +108,17 @@ function validateCustomProviderConfig(providerId, config, options = {}) {
|
|
|
103
108
|
if (!modelName) {
|
|
104
109
|
return { ok: false, error: `Model "${trimmedId}" requires a name` };
|
|
105
110
|
}
|
|
106
|
-
normalizedModels[trimmedId] = { name: modelName };
|
|
111
|
+
normalizedModels[trimmedId] = { modelID: trimmedId, name: modelName };
|
|
112
|
+
// Present means the caller owns the levels; an empty list removes them.
|
|
113
|
+
if (Array.isArray(modelValue.variants)) {
|
|
114
|
+
normalizedModels[trimmedId].variants = toModelVariants(modelValue.variants);
|
|
115
|
+
}
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
const normalized = {
|
|
110
|
-
npm,
|
|
119
|
+
package: toProviderPackage(npm),
|
|
111
120
|
name,
|
|
112
|
-
|
|
113
|
-
baseURL,
|
|
114
|
-
},
|
|
121
|
+
settings: { baseURL },
|
|
115
122
|
models: normalizedModels,
|
|
116
123
|
};
|
|
117
124
|
|
|
@@ -133,9 +140,10 @@ function validateCustomProviderConfig(providerId, config, options = {}) {
|
|
|
133
140
|
};
|
|
134
141
|
}
|
|
135
142
|
|
|
136
|
-
|
|
143
|
+
const headerSource = isPlainObject(config.headers) ? config.headers : settingsBlock.headers;
|
|
144
|
+
if (isPlainObject(headerSource)) {
|
|
137
145
|
const headers = {};
|
|
138
|
-
for (const [headerKey, headerValue] of Object.entries(
|
|
146
|
+
for (const [headerKey, headerValue] of Object.entries(headerSource)) {
|
|
139
147
|
if (typeof headerKey !== 'string' || !headerKey.trim()) {
|
|
140
148
|
continue;
|
|
141
149
|
}
|
|
@@ -145,7 +153,7 @@ function validateCustomProviderConfig(providerId, config, options = {}) {
|
|
|
145
153
|
headers[headerKey.trim()] = headerValue.trim();
|
|
146
154
|
}
|
|
147
155
|
if (Object.keys(headers).length > 0) {
|
|
148
|
-
normalized.
|
|
156
|
+
normalized.headers = headers;
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
159
|
|
|
@@ -153,34 +161,38 @@ function validateCustomProviderConfig(providerId, config, options = {}) {
|
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
function mergeCustomProviderConfig(existingValue, normalizedConfig) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
if (!Object.prototype.hasOwnProperty.call(normalizedOptions, 'headers')) {
|
|
161
|
-
delete mergedOptions.headers;
|
|
162
|
-
}
|
|
164
|
+
// Read the existing entry through the v2 projection so a legacy
|
|
165
|
+
// `npm`/`api`/`options` block is carried forward in native shape.
|
|
166
|
+
const existing = toProviderEntity(existingValue);
|
|
167
|
+
const mergedSettings = { ...(existing.settings ?? {}), ...(normalizedConfig.settings ?? {}) };
|
|
163
168
|
|
|
164
169
|
const existingModels = isPlainObject(existing.models) ? existing.models : {};
|
|
165
170
|
const normalizedModels = isPlainObject(normalizedConfig.models) ? normalizedConfig.models : {};
|
|
166
171
|
const mergedModels = Object.fromEntries(
|
|
167
172
|
Object.entries(normalizedModels).map(([modelId, normalizedModel]) => {
|
|
168
173
|
const existingModel = isPlainObject(existingModels[modelId]) ? existingModels[modelId] : {};
|
|
169
|
-
const
|
|
170
|
-
|
|
174
|
+
const mergedModel = { ...existingModel, ...normalizedModel };
|
|
175
|
+
if (Array.isArray(mergedModel.variants) && mergedModel.variants.length === 0) {
|
|
176
|
+
delete mergedModel.variants;
|
|
177
|
+
}
|
|
178
|
+
return [modelId, mergedModel];
|
|
171
179
|
}),
|
|
172
180
|
);
|
|
173
181
|
|
|
174
182
|
const merged = {
|
|
175
183
|
...existing,
|
|
176
184
|
...normalizedConfig,
|
|
177
|
-
|
|
185
|
+
settings: mergedSettings,
|
|
178
186
|
models: mergedModels,
|
|
179
187
|
};
|
|
188
|
+
// Headers and env are explicit removals when the form omits them.
|
|
189
|
+
if (!Object.prototype.hasOwnProperty.call(normalizedConfig, 'headers')) {
|
|
190
|
+
delete merged.headers;
|
|
191
|
+
}
|
|
180
192
|
if (!Object.prototype.hasOwnProperty.call(normalizedConfig, 'env')) {
|
|
181
193
|
delete merged.env;
|
|
182
194
|
}
|
|
183
|
-
return merged;
|
|
195
|
+
return toProviderEntity(merged);
|
|
184
196
|
}
|
|
185
197
|
|
|
186
198
|
/**
|
|
@@ -213,20 +225,11 @@ function upsertProviderConfig(providerId, config, workingDirectory, scope = 'use
|
|
|
213
225
|
}
|
|
214
226
|
|
|
215
227
|
const targetConfig = getConfigForPath(layers, targetPath);
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
targetConfig.provider = providerConfig;
|
|
222
|
-
if (Object.prototype.hasOwnProperty.call(providersAlias, validated.value.providerId)) {
|
|
223
|
-
delete providersAlias[validated.value.providerId];
|
|
224
|
-
if (Object.keys(providersAlias).length === 0) {
|
|
225
|
-
delete targetConfig.providers;
|
|
226
|
-
} else {
|
|
227
|
-
targetConfig.providers = providersAlias;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
228
|
+
const existing = readSectionEntry(targetConfig, 'providers', validated.value.providerId).value;
|
|
229
|
+
const mergedConfig = mergeCustomProviderConfig(existing, validated.value.config);
|
|
230
|
+
// Writes `providers`; a legacy `provider.<id>` in the same file is dropped so
|
|
231
|
+
// the two spellings cannot disagree.
|
|
232
|
+
writeSectionEntry(targetConfig, 'providers', validated.value.providerId, mergedConfig);
|
|
230
233
|
|
|
231
234
|
if (Array.isArray(targetConfig.disabled_providers)) {
|
|
232
235
|
targetConfig.disabled_providers = targetConfig.disabled_providers.filter(
|
|
@@ -265,33 +268,10 @@ function removeProviderConfig(providerId, workingDirectory, scope = 'user') {
|
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
const targetConfig = getConfigForPath(layers, targetPath);
|
|
268
|
-
|
|
269
|
-
const providersConfig = isPlainObject(targetConfig.providers) ? targetConfig.providers : {};
|
|
270
|
-
const removedProvider = Object.prototype.hasOwnProperty.call(providerConfig, providerId);
|
|
271
|
-
const removedProviders = Object.prototype.hasOwnProperty.call(providersConfig, providerId);
|
|
272
|
-
|
|
273
|
-
if (!removedProvider && !removedProviders) {
|
|
271
|
+
if (!deleteSectionEntry(targetConfig, 'providers', providerId)) {
|
|
274
272
|
return false;
|
|
275
273
|
}
|
|
276
274
|
|
|
277
|
-
if (removedProvider) {
|
|
278
|
-
delete providerConfig[providerId];
|
|
279
|
-
if (Object.keys(providerConfig).length === 0) {
|
|
280
|
-
delete targetConfig.provider;
|
|
281
|
-
} else {
|
|
282
|
-
targetConfig.provider = providerConfig;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (removedProviders) {
|
|
287
|
-
delete providersConfig[providerId];
|
|
288
|
-
if (Object.keys(providersConfig).length === 0) {
|
|
289
|
-
delete targetConfig.providers;
|
|
290
|
-
} else {
|
|
291
|
-
targetConfig.providers = providersConfig;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
275
|
writeConfig(targetConfig, targetPath || CONFIG_FILE);
|
|
296
276
|
console.log(`Removed provider ${providerId} from config: ${targetPath}`);
|
|
297
277
|
return true;
|
|
@@ -82,7 +82,7 @@ describe('custom provider config persistence', () => {
|
|
|
82
82
|
models: { m: { name: 'M' } },
|
|
83
83
|
});
|
|
84
84
|
expect(result.ok).toBe(true);
|
|
85
|
-
expect(result.value.config.
|
|
85
|
+
expect(result.value.config.package).toBe(`aisdk:${npm}`);
|
|
86
86
|
}
|
|
87
87
|
});
|
|
88
88
|
|
|
@@ -118,16 +118,15 @@ describe('custom provider config persistence', () => {
|
|
|
118
118
|
expect(result.path.startsWith(projectDir)).toBe(true);
|
|
119
119
|
|
|
120
120
|
const written = readJson(result.path);
|
|
121
|
-
expect(written.provider
|
|
122
|
-
|
|
121
|
+
expect(written.provider).toBeUndefined();
|
|
122
|
+
expect(written.providers['campus-llm']).toEqual({
|
|
123
123
|
name: 'Campus LLM',
|
|
124
|
+
package: 'aisdk:@ai-sdk/openai-compatible',
|
|
124
125
|
env: ['CAMPUS_KEY'],
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
headers: { 'X-Campus': '1' },
|
|
128
|
-
},
|
|
126
|
+
settings: { baseURL: 'https://llm.example.edu/v1' },
|
|
127
|
+
headers: { 'X-Campus': '1' },
|
|
129
128
|
models: {
|
|
130
|
-
'fast-model': { name: 'Fast' },
|
|
129
|
+
'fast-model': { modelID: 'fast-model', name: 'Fast' },
|
|
131
130
|
},
|
|
132
131
|
});
|
|
133
132
|
|
|
@@ -136,8 +135,32 @@ describe('custom provider config persistence', () => {
|
|
|
136
135
|
expect(sources.sources.project.path).toBe(result.path);
|
|
137
136
|
});
|
|
138
137
|
|
|
138
|
+
test('upsertProviderConfig writes, keeps and clears model variants', () => {
|
|
139
|
+
const base = {
|
|
140
|
+
name: 'Campus LLM',
|
|
141
|
+
npm: '@ai-sdk/openai-compatible',
|
|
142
|
+
options: { baseURL: 'https://llm.example.edu/v1' },
|
|
143
|
+
env: ['CAMPUS_KEY'],
|
|
144
|
+
};
|
|
145
|
+
const result = upsertProviderConfig('campus-llm', {
|
|
146
|
+
...base,
|
|
147
|
+
models: { m: { name: 'M', variants: [{ id: 'low', settings: { reasoningEffort: 'low' } }] } },
|
|
148
|
+
}, projectDir, 'project');
|
|
149
|
+
expect(readJson(result.path).providers['campus-llm'].models.m.variants).toEqual([
|
|
150
|
+
{ id: 'low', settings: { reasoningEffort: 'low' } },
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
upsertProviderConfig('campus-llm', { ...base, models: { m: { name: 'M' } } }, projectDir, 'project');
|
|
154
|
+
expect(readJson(result.path).providers['campus-llm'].models.m.variants).toEqual([
|
|
155
|
+
{ id: 'low', settings: { reasoningEffort: 'low' } },
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
upsertProviderConfig('campus-llm', { ...base, models: { m: { name: 'M', variants: [] } } }, projectDir, 'project');
|
|
159
|
+
expect(readJson(result.path).providers['campus-llm'].models.m).toEqual({ modelID: 'm', name: 'M' });
|
|
160
|
+
});
|
|
161
|
+
|
|
139
162
|
test('upsertProviderConfig updates existing entry and clears disabled_providers', () => {
|
|
140
|
-
const configPath = path.join(projectDir, 'opencode.json');
|
|
163
|
+
const configPath = path.join(projectDir, '.opencode', 'opencode.json');
|
|
141
164
|
writeJson(configPath, {
|
|
142
165
|
provider: {
|
|
143
166
|
'campus-llm': {
|
|
@@ -158,13 +181,14 @@ describe('custom provider config persistence', () => {
|
|
|
158
181
|
}, projectDir, 'project');
|
|
159
182
|
|
|
160
183
|
const written = readJson(configPath);
|
|
161
|
-
expect(written.provider
|
|
162
|
-
expect(written.
|
|
184
|
+
expect(written.provider).toBeUndefined();
|
|
185
|
+
expect(written.providers['campus-llm'].name).toBe('Campus LLM');
|
|
186
|
+
expect(written.providers['campus-llm'].models).toEqual({ b: { modelID: 'b', name: 'B' } });
|
|
163
187
|
expect(written.disabled_providers).toEqual(['other']);
|
|
164
188
|
});
|
|
165
189
|
|
|
166
190
|
test('upsertProviderConfig preserves unmanaged provider and model metadata', () => {
|
|
167
|
-
const configPath = path.join(projectDir, 'opencode.json');
|
|
191
|
+
const configPath = path.join(projectDir, '.opencode', 'opencode.json');
|
|
168
192
|
writeJson(configPath, {
|
|
169
193
|
provider: {
|
|
170
194
|
'campus-llm': {
|
|
@@ -210,69 +234,116 @@ describe('custom provider config persistence', () => {
|
|
|
210
234
|
},
|
|
211
235
|
}, projectDir, 'project', { hasStoredAuth: true });
|
|
212
236
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
237
|
+
// A legacy `provider` entry is rewritten in place into `providers` in v2
|
|
238
|
+
// shape: `npm` -> `package`, `options` -> `settings`, `tool_call`/
|
|
239
|
+
// `modalities` -> `capabilities`, variants map -> variants array. Fields v2
|
|
240
|
+
// accepts but ignores (`reasoning`, `attachment`) are dropped.
|
|
241
|
+
const config = readJson(configPath);
|
|
242
|
+
expect(config.provider).toBeUndefined();
|
|
243
|
+
expect(config.providers['campus-llm']).toEqual({
|
|
216
244
|
name: 'Campus LLM',
|
|
217
|
-
|
|
218
|
-
|
|
245
|
+
package: 'aisdk:@ai-sdk/openai-compatible',
|
|
246
|
+
settings: {
|
|
219
247
|
baseURL: 'https://new.example.edu/v1',
|
|
220
248
|
timeout: 45_000,
|
|
221
249
|
},
|
|
222
250
|
models: {
|
|
223
251
|
retained: {
|
|
252
|
+
modelID: 'retained',
|
|
224
253
|
name: 'Retained model',
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
254
|
+
settings: { instructions: 'Keep this instruction' },
|
|
255
|
+
capabilities: { tools: true, input: ['text', 'image', 'pdf'], output: ['text'] },
|
|
256
|
+
variants: [
|
|
257
|
+
{ id: 'low', settings: { reasoningEffort: 'low' } },
|
|
258
|
+
{ id: 'high', settings: { reasoningEffort: 'high' } },
|
|
259
|
+
],
|
|
229
260
|
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
261
|
+
},
|
|
262
|
+
added: { modelID: 'added', name: 'Added model' },
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('renaming a provider keeps canonical and model compatibility', () => {
|
|
268
|
+
const configPath = path.join(projectDir, '.opencode', 'opencode.json');
|
|
269
|
+
writeJson(configPath, {
|
|
270
|
+
providers: {
|
|
271
|
+
'openai-proxy': {
|
|
272
|
+
canonical: 'openai',
|
|
273
|
+
name: 'Old name',
|
|
274
|
+
package: 'aisdk:@ai-sdk/openai-compatible',
|
|
275
|
+
env: ['PROXY_KEY'],
|
|
276
|
+
settings: { baseURL: 'https://proxy.example.com/v1' },
|
|
277
|
+
models: {
|
|
278
|
+
'gpt-5': {
|
|
279
|
+
modelID: 'gpt-5',
|
|
280
|
+
name: 'GPT-5',
|
|
281
|
+
compatibility: { reasoningField: 'reasoning_content', requireReasoning: true, maxTokensField: 'max_tokens' },
|
|
282
|
+
},
|
|
234
283
|
},
|
|
235
|
-
customModelField: { source: 'manual' },
|
|
236
284
|
},
|
|
237
|
-
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
upsertProviderConfig('openai-proxy', {
|
|
289
|
+
name: 'New name',
|
|
290
|
+
env: ['PROXY_KEY'],
|
|
291
|
+
settings: { baseURL: 'https://proxy.example.com/v1' },
|
|
292
|
+
models: { 'gpt-5': { name: 'GPT-5' } },
|
|
293
|
+
}, projectDir, 'project');
|
|
294
|
+
|
|
295
|
+
expect(readJson(configPath).providers['openai-proxy']).toEqual({
|
|
296
|
+
canonical: 'openai',
|
|
297
|
+
name: 'New name',
|
|
298
|
+
package: 'aisdk:@ai-sdk/openai-compatible',
|
|
299
|
+
env: ['PROXY_KEY'],
|
|
300
|
+
settings: { baseURL: 'https://proxy.example.com/v1' },
|
|
301
|
+
models: {
|
|
302
|
+
'gpt-5': {
|
|
303
|
+
modelID: 'gpt-5',
|
|
304
|
+
name: 'GPT-5',
|
|
305
|
+
compatibility: { reasoningField: 'reasoning_content', requireReasoning: true, maxTokensField: 'max_tokens' },
|
|
306
|
+
},
|
|
238
307
|
},
|
|
239
308
|
});
|
|
240
309
|
});
|
|
241
310
|
|
|
242
|
-
test('
|
|
243
|
-
const configPath = path.join(projectDir, 'opencode.json');
|
|
311
|
+
test('accepts a native v2 payload and keeps the entry in providers', () => {
|
|
312
|
+
const configPath = path.join(projectDir, '.opencode', 'opencode.json');
|
|
244
313
|
writeJson(configPath, {
|
|
245
314
|
providers: {
|
|
246
|
-
|
|
247
|
-
name: '
|
|
248
|
-
|
|
249
|
-
|
|
315
|
+
native: {
|
|
316
|
+
name: 'Native provider',
|
|
317
|
+
package: 'aisdk:@ai-sdk/openai-compatible',
|
|
318
|
+
settings: { baseURL: 'https://old.example.com/v1', timeout: 30_000 },
|
|
319
|
+
models: { model: { modelID: 'model', name: 'Old model' } },
|
|
250
320
|
},
|
|
251
321
|
},
|
|
252
322
|
});
|
|
253
323
|
|
|
254
|
-
upsertProviderConfig('
|
|
324
|
+
upsertProviderConfig('native', {
|
|
255
325
|
name: 'Updated provider',
|
|
256
|
-
|
|
326
|
+
package: 'aisdk:@ai-sdk/openai-compatible',
|
|
327
|
+
settings: { baseURL: 'https://new.example.com/v1' },
|
|
257
328
|
models: { model: { name: 'Updated model' } },
|
|
258
329
|
}, projectDir, 'project', { hasStoredAuth: true });
|
|
259
330
|
|
|
260
331
|
const written = readJson(configPath);
|
|
261
|
-
expect(written.
|
|
262
|
-
expect(written.
|
|
263
|
-
npm: '@ai-sdk/openai-compatible',
|
|
332
|
+
expect(written.provider).toBeUndefined();
|
|
333
|
+
expect(written.providers.native).toEqual({
|
|
264
334
|
name: 'Updated provider',
|
|
265
|
-
|
|
266
|
-
|
|
335
|
+
package: 'aisdk:@ai-sdk/openai-compatible',
|
|
336
|
+
settings: { baseURL: 'https://new.example.com/v1', timeout: 30_000 },
|
|
337
|
+
models: { model: { modelID: 'model', name: 'Updated model' } },
|
|
267
338
|
});
|
|
268
339
|
});
|
|
269
340
|
|
|
270
|
-
test('
|
|
271
|
-
const configPath = path.join(projectDir, 'opencode.json');
|
|
341
|
+
test('rewriting one legacy provider entry leaves the other legacy entries alone', () => {
|
|
342
|
+
const configPath = path.join(projectDir, '.opencode', 'opencode.json');
|
|
272
343
|
writeJson(configPath, {
|
|
273
|
-
|
|
274
|
-
legacy: { name: 'Legacy provider', options: { baseURL: 'https://old.example.com/v1' }, models: { model: { name: 'Old model' } } },
|
|
275
|
-
untouched: { name: 'Untouched', options: { baseURL: 'https://other.example.com/v1' }, models: { model: { name: 'Other model' } } },
|
|
344
|
+
provider: {
|
|
345
|
+
legacy: { npm: '@ai-sdk/openai-compatible', name: 'Legacy provider', options: { baseURL: 'https://old.example.com/v1' }, models: { model: { name: 'Old model' } } },
|
|
346
|
+
untouched: { npm: '@ai-sdk/openai-compatible', name: 'Untouched', options: { baseURL: 'https://other.example.com/v1' }, models: { model: { name: 'Other model' } } },
|
|
276
347
|
},
|
|
277
348
|
});
|
|
278
349
|
|
|
@@ -283,10 +354,10 @@ describe('custom provider config persistence', () => {
|
|
|
283
354
|
}, projectDir, 'project', { hasStoredAuth: true });
|
|
284
355
|
|
|
285
356
|
const written = readJson(configPath);
|
|
286
|
-
expect(written.
|
|
287
|
-
untouched: { name: 'Untouched', options: { baseURL: 'https://other.example.com/v1' }, models: { model: { name: 'Other model' } } },
|
|
357
|
+
expect(written.provider).toEqual({
|
|
358
|
+
untouched: { npm: '@ai-sdk/openai-compatible', name: 'Untouched', options: { baseURL: 'https://other.example.com/v1' }, models: { model: { name: 'Other model' } } },
|
|
288
359
|
});
|
|
289
|
-
expect(written.
|
|
360
|
+
expect(written.providers.legacy.name).toBe('Updated provider');
|
|
290
361
|
});
|
|
291
362
|
|
|
292
363
|
test('upsert then remove restores absence', () => {
|
|
@@ -303,7 +374,7 @@ describe('custom provider config persistence', () => {
|
|
|
303
374
|
});
|
|
304
375
|
|
|
305
376
|
test('failed validation does not write config', () => {
|
|
306
|
-
const configPath = path.join(projectDir, 'opencode.json');
|
|
377
|
+
const configPath = path.join(projectDir, '.opencode', 'opencode.json');
|
|
307
378
|
expect(() => upsertProviderConfig('ok', {
|
|
308
379
|
name: 'X',
|
|
309
380
|
options: { baseURL: 'not-a-url' },
|
|
@@ -326,7 +397,7 @@ describe('custom provider config persistence', () => {
|
|
|
326
397
|
|
|
327
398
|
test('project-scope edit updates project layer without creating a user entry', () => {
|
|
328
399
|
const providerId = `proj-scope-${Date.now()}`;
|
|
329
|
-
const configPath = path.join(projectDir, 'opencode.json');
|
|
400
|
+
const configPath = path.join(projectDir, '.opencode', 'opencode.json');
|
|
330
401
|
|
|
331
402
|
upsertProviderConfig(providerId, {
|
|
332
403
|
name: 'Project Scoped',
|
|
@@ -341,14 +412,12 @@ describe('custom provider config persistence', () => {
|
|
|
341
412
|
}, projectDir, 'project', { hasStoredAuth: true });
|
|
342
413
|
|
|
343
414
|
const written = readJson(configPath);
|
|
344
|
-
expect(written.
|
|
345
|
-
npm: '@ai-sdk/openai-compatible',
|
|
415
|
+
expect(written.providers[providerId]).toEqual({
|
|
346
416
|
name: 'Project Scoped Updated',
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
},
|
|
351
|
-
models: { m: { name: 'M2' } },
|
|
417
|
+
package: 'aisdk:@ai-sdk/openai-compatible',
|
|
418
|
+
settings: { baseURL: 'https://project.example.com/v2' },
|
|
419
|
+
headers: { 'X-Project': '1' },
|
|
420
|
+
models: { m: { modelID: 'm', name: 'M2' } },
|
|
352
421
|
});
|
|
353
422
|
|
|
354
423
|
const sources = getProviderSources(providerId, projectDir);
|
|
@@ -387,8 +456,8 @@ describe('custom provider config persistence', () => {
|
|
|
387
456
|
}, projectDir, 'custom', { hasStoredAuth: true });
|
|
388
457
|
|
|
389
458
|
const written = readJson(customPath);
|
|
390
|
-
expect(written.
|
|
391
|
-
expect(written.
|
|
459
|
+
expect(written.providers[providerId].name).toBe('Custom Scoped Updated');
|
|
460
|
+
expect(written.providers[providerId].settings.baseURL).toBe('https://custom.example.com/v2');
|
|
392
461
|
|
|
393
462
|
const sources = getProviderSources(providerId, projectDir);
|
|
394
463
|
expect(sources.sources.custom.exists).toBe(true);
|