@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -7
- package/bin/cli-opencode.test.js +208 -0
- package/bin/cli.js +62 -9
- package/bin/lib/cli-startup.js +105 -2
- package/bin/lib/cli-startup.test.js +179 -0
- package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
- package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
- package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
- package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
- package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
- package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
- package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
- package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
- package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
- package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
- package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
- package/dist/assets/SettingsView-BRxELjow.js +25 -0
- package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
- package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
- package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
- package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
- package/dist/assets/collapsible-DXbDynN4.js +3 -0
- package/dist/assets/context-menu-CqolsLCm.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
- package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
- package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-CM42qUed.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
- package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
- package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/main-BILgBI3B.js +169 -0
- package/dist/assets/main-BeveiW6F.js +2 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
- package/dist/assets/miniChat-BgrzM3u7.js +2 -0
- package/dist/assets/mobile-DtYoQE8C.js +2 -0
- package/dist/assets/multirun-jy8dEI96.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-BQghHRfp.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
- package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
- package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
- package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
- package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
- package/dist/assets/src-CLN7Z-zx.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
- package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
- package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
- package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
- package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
- package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +2 -0
- package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
- package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.js} +2 -2
- package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
- package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
- package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
- package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
- package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
- package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
- package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
- package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
- package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
- package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
- package/dist/index.html +13 -13
- package/dist/mini-chat.html +13 -13
- package/dist/mobile.html +13 -13
- package/package.json +2 -2
- package/server/index.js +392 -20
- package/server/lib/assistants/DOCUMENTATION.md +26 -21
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +454 -0
- package/server/lib/assistants/routes.js +11 -5
- package/server/lib/assistants/service.js +850 -294
- package/server/lib/assistants/service.test.js +588 -274
- package/server/lib/conversations/DOCUMENTATION.md +5 -5
- package/server/lib/conversations/service.js +87 -57
- package/server/lib/conversations/service.test.js +132 -100
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +51 -5
- package/server/lib/event-stream/runtime.js +57 -20
- package/server/lib/event-stream/runtime.test.js +112 -0
- package/server/lib/llm/DOCUMENTATION.md +95 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +41 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +516 -453
- package/server/lib/llm/generate.test.js +705 -484
- package/server/lib/llm/routes.js +8 -4
- package/server/lib/message-queue/DOCUMENTATION.md +5 -5
- package/server/lib/message-queue/opencode-adapter.js +191 -104
- package/server/lib/message-queue/opencode-adapter.test.js +328 -58
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/message-queue/runtime.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/notifications/runtime.js +5 -0
- package/server/lib/opencode/DOCUMENTATION.md +103 -16
- package/server/lib/opencode/agent-document.d.ts +28 -0
- package/server/lib/opencode/agent-document.js +280 -0
- package/server/lib/opencode/agent-document.test.js +137 -0
- package/server/lib/opencode/agents.js +96 -0
- package/server/lib/opencode/auth-state-runtime.js +5 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/config-entity-routes.js +111 -102
- package/server/lib/opencode/config-entity-routes.test.js +123 -10
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +238 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +208 -111
- package/server/lib/opencode/env-runtime.test.js +211 -14
- package/server/lib/opencode/feature-routes-runtime.js +80 -5
- package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
- package/server/lib/opencode/http-server-connections.js +98 -0
- package/server/lib/opencode/http-server-connections.test.js +321 -0
- package/server/lib/opencode/lifecycle.js +539 -95
- package/server/lib/opencode/lifecycle.test.js +698 -6
- package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
- package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
- package/server/lib/opencode/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +88 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
- package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
- package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
- package/server/lib/opencode/plugin-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- package/server/lib/opencode/provider-catalog.js +58 -12
- package/server/lib/opencode/provider-catalog.test.js +48 -0
- package/server/lib/opencode/proxy.js +368 -28
- package/server/lib/opencode/proxy.test.js +338 -0
- package/server/lib/opencode/routes.js +303 -76
- package/server/lib/opencode/routes.test.js +315 -50
- package/server/lib/opencode/runtime-contract.js +401 -0
- package/server/lib/opencode/runtime-contract.test.js +214 -0
- package/server/lib/opencode/server-opencode-fetch.js +242 -0
- package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
- package/server/lib/opencode/server-utils-runtime.js +35 -0
- package/server/lib/opencode/shared-service.integration.test.js +66 -0
- package/server/lib/opencode/shared-service.js +147 -0
- package/server/lib/opencode/shared-service.test.js +93 -0
- package/server/lib/opencode/shared.js +3 -3
- package/server/lib/opencode/shutdown-runtime.js +15 -6
- package/server/lib/opencode/shutdown-runtime.test.js +49 -1
- package/server/lib/opencode/skill-routes.js +26 -40
- package/server/lib/opencode/skill-routes.test.js +71 -7
- package/server/lib/opencode/skills.test.js +15 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
- package/server/lib/opencode/static-routes-runtime.js +3 -1
- package/server/lib/opencode/static-routes-runtime.test.js +33 -0
- package/server/lib/opencode/v1-migration-gate.js +157 -0
- package/server/lib/opencode/v1-migration-gate.test.js +111 -0
- package/server/lib/opencode/v2-client.js +60 -0
- package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
- package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
- package/server/lib/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
- package/server/lib/permission-auto-accept/runtime.js +14 -7
- package/server/lib/permission-auto-accept/runtime.test.js +27 -12
- package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
- package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
- package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
- package/server/lib/scheduled-tasks/routes.js +27 -0
- package/server/lib/scheduled-tasks/routes.test.js +94 -0
- package/server/lib/scheduled-tasks/runtime.js +381 -310
- package/server/lib/scheduled-tasks/runtime.test.js +600 -309
- package/server/lib/session-goal/DOCUMENTATION.md +173 -22
- package/server/lib/session-goal/capability.js +57 -0
- package/server/lib/session-goal/capability.test.js +33 -0
- package/server/lib/session-goal/routes.js +240 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +1617 -113
- package/server/lib/session-goal/runtime.test.js +1500 -70
- package/server/lib/session-index/DOCUMENTATION.md +18 -15
- package/server/lib/session-index/event-ingest.js +89 -16
- package/server/lib/session-index/event-ingest.test.js +73 -0
- package/server/lib/session-index/host-authority-snapshot.js +92 -0
- package/server/lib/session-index/routes.host-archive.test.js +201 -0
- package/server/lib/session-index/routes.js +63 -10
- package/server/lib/session-index/routes.test.js +8 -8
- package/server/lib/session-index/service.js +7 -1
- package/server/lib/session-index/sync-runtime.js +171 -41
- package/server/lib/session-index/sync-runtime.test.js +164 -9
- package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
- package/server/lib/session-metadata/routes.js +193 -0
- package/server/lib/session-metadata/routes.test.js +156 -0
- package/server/lib/session-metadata/session-archive.d.ts +73 -0
- package/server/lib/session-metadata/session-archive.js +654 -0
- package/server/lib/session-metadata/session-archive.test.js +359 -0
- package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
- package/server/lib/session-metadata/session-metadata-store.js +375 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
- package/server/lib/session-metadata/session-projection.d.ts +41 -0
- package/server/lib/session-metadata/session-projection.js +255 -0
- package/server/lib/session-metadata/session-projection.test.js +64 -0
- package/server/lib/session-metadata/system-session.js +55 -0
- package/server/lib/session-metadata/system-session.test.js +55 -0
- package/server/lib/session-title/DOCUMENTATION.md +40 -7
- package/server/lib/session-title/native-runtime.test.js +167 -0
- package/server/lib/session-title/runtime.js +67 -42
- package/server/lib/session-title/session-access.js +46 -0
- package/server/lib/session-title/session-access.test.js +91 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
- package/server/lib/session-turn-pages/reconcile.service.js +1 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
- package/server/lib/session-turn-pages/routes.js +123 -75
- package/server/lib/session-turn-pages/routes.test.js +566 -0
- package/server/lib/session-turn-pages/service.js +6 -2
- package/server/lib/session-turn-pages/service.test.js +34 -0
- package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
- package/server/lib/session-turn-pages/session-message-projection.js +281 -0
- package/server/lib/small-model/DOCUMENTATION.md +101 -146
- package/server/lib/small-model/catalog.js +84 -54
- package/server/lib/small-model/catalog.test.js +111 -9
- package/server/lib/small-model/custom-api.js +68 -0
- package/server/lib/small-model/custom-api.test.js +64 -0
- package/server/lib/small-model/index.js +125 -241
- package/server/lib/small-model/resolve.js +35 -120
- package/server/lib/small-model/resolve.test.js +20 -110
- package/server/lib/small-model/routes.js +1 -3
- package/server/lib/small-model/summary-settings.test.js +157 -253
- package/server/opencode-proxy.test.js +63 -12
- package/dist/assets/AssistantView-BUveAgc4.js +0 -3
- package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
- package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
- package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
- package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
- package/dist/assets/SettingsView-PkXIavr7.js +0 -25
- package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
- package/dist/assets/collapsible-D5Phaykh.js +0 -1
- package/dist/assets/context-menu-CubSDW2g.js +0 -1
- package/dist/assets/es-DA3MqOo0.js +0 -15
- package/dist/assets/fr-Dbz0CdIA.js +0 -13
- package/dist/assets/gitApi-DXyC4MiV.js +0 -7
- package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
- package/dist/assets/ja-DumanVQA.js +0 -15
- package/dist/assets/ko-C0IkiUjQ.js +0 -15
- package/dist/assets/main-BPS5ylib.js +0 -2
- package/dist/assets/main-Cpd9VWYs.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-B8_KLWiY.js +0 -2
- package/dist/assets/mobile-CtktKrXn.js +0 -2
- package/dist/assets/multirun-CYfP9Cfm.js +0 -4
- package/dist/assets/pl-B9No6MQp.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
- package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
- package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/store-5B_LDnLM.js +0 -15
- package/dist/assets/uk-C_1KkqyP.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
- package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
- package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
- package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-uWILgBdt.js +0 -15
- package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
- package/server/lib/small-model/call.js +0 -427
- package/server/lib/small-model/call.test.js +0 -382
- package/server/lib/small-model/opencode-session.js +0 -339
- package/server/lib/small-model/opencode-session.test.js +0 -177
|
@@ -1,41 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OpenCode } from '@opencode/client';
|
|
2
2
|
|
|
3
3
|
// Directory-scoped OpenCode provider catalog for small-model resolution.
|
|
4
|
-
// Never contacts models.dev — source of truth is
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// Never contacts models.dev — source of truth is provider.list + model.list,
|
|
5
|
+
// the same connected catalog the LLM gateway checks before generating.
|
|
6
|
+
// ModelInfo.id is the external model id; ModelInfo.modelID is internal.
|
|
7
|
+
// OpenCode failure is an explicit error, never a substitute catalog.
|
|
7
8
|
|
|
8
9
|
const CATALOG_TTL_MS = 30_000;
|
|
9
10
|
const CATALOG_TIMEOUT_MS = 8_000;
|
|
10
11
|
|
|
11
|
-
// Minimal catalog when OpenCode is unreachable. OpenAI OAuth and Copilot use
|
|
12
|
-
// fixed utility models outside the catalog; Google/Anthropic need family
|
|
13
|
-
// entries so the family-priority scan can still pick a small model.
|
|
14
|
-
const MINIMAL_FALLBACK_CATALOG = Object.freeze({
|
|
15
|
-
google: Object.freeze({
|
|
16
|
-
id: 'google',
|
|
17
|
-
name: 'Google',
|
|
18
|
-
models: Object.freeze({
|
|
19
|
-
'gemini-2.5-flash': Object.freeze({
|
|
20
|
-
id: 'gemini-2.5-flash',
|
|
21
|
-
family: 'gemini-flash',
|
|
22
|
-
release_date: '2025-06-01',
|
|
23
|
-
}),
|
|
24
|
-
}),
|
|
25
|
-
}),
|
|
26
|
-
anthropic: Object.freeze({
|
|
27
|
-
id: 'anthropic',
|
|
28
|
-
name: 'Anthropic',
|
|
29
|
-
models: Object.freeze({
|
|
30
|
-
'claude-haiku-4-5': Object.freeze({
|
|
31
|
-
id: 'claude-haiku-4-5',
|
|
32
|
-
family: 'claude-haiku',
|
|
33
|
-
release_date: '2025-10-01',
|
|
34
|
-
}),
|
|
35
|
-
}),
|
|
36
|
-
}),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
12
|
const normalizeDirectoryKey = (directory) => {
|
|
40
13
|
if (typeof directory !== 'string') return '';
|
|
41
14
|
const trimmed = directory.trim();
|
|
@@ -46,6 +19,67 @@ const normalizeDirectoryKey = (directory) => {
|
|
|
46
19
|
|
|
47
20
|
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
48
21
|
|
|
22
|
+
// v2 ModelInfo.cost is a tier array. Ranking still reads cost.input / cost.output.
|
|
23
|
+
const projectCatalogCost = (cost) => {
|
|
24
|
+
const entry = Array.isArray(cost)
|
|
25
|
+
? (cost.find((item) => isRecord(item) && !isRecord(item.tier)) || cost.find((item) => isRecord(item)))
|
|
26
|
+
: (isRecord(cost) ? cost : null);
|
|
27
|
+
if (!isRecord(entry)) return undefined;
|
|
28
|
+
const projected = Object.create(null);
|
|
29
|
+
if (Number.isFinite(entry.input)) projected.input = entry.input;
|
|
30
|
+
if (Number.isFinite(entry.output)) projected.output = entry.output;
|
|
31
|
+
return Object.keys(projected).length > 0 ? projected : undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Compose official v2 provider.list + model.list into the SDK catalog shape
|
|
35
|
+
// consumed by toSmallModelCatalog. Missing arrays are failure, not empty success.
|
|
36
|
+
const composeV2ProviderCatalogSource = (providers, models) => {
|
|
37
|
+
if (!Array.isArray(providers) || !Array.isArray(models)) return null;
|
|
38
|
+
|
|
39
|
+
const modelsByProvider = new Map();
|
|
40
|
+
for (const model of models) {
|
|
41
|
+
if (!isRecord(model)) continue;
|
|
42
|
+
const providerID = typeof model.providerID === 'string' ? model.providerID : '';
|
|
43
|
+
// ModelInfo.id is the external model id used in generate/session refs.
|
|
44
|
+
// ModelInfo.modelID is a separate internal field and must not be preferred.
|
|
45
|
+
const modelID = typeof model.id === 'string' && model.id
|
|
46
|
+
? model.id
|
|
47
|
+
: (typeof model.modelID === 'string' ? model.modelID : '');
|
|
48
|
+
if (!providerID || !modelID) continue;
|
|
49
|
+
if (!modelsByProvider.has(providerID)) modelsByProvider.set(providerID, Object.create(null));
|
|
50
|
+
const entry = { id: modelID };
|
|
51
|
+
if (typeof model.family === 'string' && model.family) entry.family = model.family;
|
|
52
|
+
if (isRecord(model.limit)) {
|
|
53
|
+
const limit = Object.create(null);
|
|
54
|
+
if (Number.isFinite(model.limit.context)) limit.context = model.limit.context;
|
|
55
|
+
if (Number.isFinite(model.limit.output)) limit.output = model.limit.output;
|
|
56
|
+
if (Object.keys(limit).length > 0) entry.limit = limit;
|
|
57
|
+
}
|
|
58
|
+
const cost = projectCatalogCost(model.cost);
|
|
59
|
+
if (cost) entry.cost = cost;
|
|
60
|
+
if (isRecord(model.api) && typeof model.api.url === 'string' && model.api.url.trim()) {
|
|
61
|
+
entry.api = { url: model.api.url.trim() };
|
|
62
|
+
}
|
|
63
|
+
if (typeof model.time?.released === 'number' && Number.isFinite(model.time.released)) {
|
|
64
|
+
entry.release_date = new Date(model.time.released).toISOString().slice(0, 10);
|
|
65
|
+
} else if (typeof model.release_date === 'string' && model.release_date) {
|
|
66
|
+
entry.release_date = model.release_date;
|
|
67
|
+
}
|
|
68
|
+
modelsByProvider.get(providerID)[modelID] = entry;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const catalogProviders = [];
|
|
72
|
+
for (const provider of providers) {
|
|
73
|
+
if (!isRecord(provider) || typeof provider.id !== 'string' || !provider.id) continue;
|
|
74
|
+
catalogProviders.push({
|
|
75
|
+
id: provider.id,
|
|
76
|
+
name: typeof provider.name === 'string' && provider.name ? provider.name : provider.id,
|
|
77
|
+
models: modelsByProvider.get(provider.id) || Object.create(null),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return { providers: catalogProviders };
|
|
81
|
+
};
|
|
82
|
+
|
|
49
83
|
/**
|
|
50
84
|
* Convert the official SDK provider list into the internal small-model catalog
|
|
51
85
|
* shape. Keeps family / release_date / limit / cost.input|output /
|
|
@@ -121,22 +155,27 @@ export function createModelCatalogLoader({
|
|
|
121
155
|
timeoutMs = CATALOG_TIMEOUT_MS,
|
|
122
156
|
fetchImpl = globalThis.fetch.bind(globalThis),
|
|
123
157
|
}) {
|
|
124
|
-
/** @type {Map<string, { catalog: object, cachedAt: number,
|
|
158
|
+
/** @type {Map<string, { catalog: object | null, cachedAt: number, inflight: Promise<object> | null }>} */
|
|
125
159
|
const buckets = new Map();
|
|
126
160
|
|
|
127
161
|
const fetchOpenCodeCatalog = async (directory) => {
|
|
128
162
|
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
129
|
-
const client =
|
|
163
|
+
const client = OpenCode.make({
|
|
130
164
|
baseUrl,
|
|
131
|
-
...(directory ? { directory } : {}),
|
|
132
165
|
headers: getOpenCodeAuthHeaders(),
|
|
133
|
-
fetch: (request) => fetchImpl(request, { signal: AbortSignal.timeout(timeoutMs) }),
|
|
166
|
+
fetch: (request, init) => fetchImpl(request, { ...init, signal: AbortSignal.timeout(timeoutMs) }),
|
|
134
167
|
});
|
|
135
|
-
const
|
|
136
|
-
|
|
168
|
+
const location = directory ? { directory } : undefined;
|
|
169
|
+
const request = location ? { location } : undefined;
|
|
170
|
+
const [providersResult, modelsResult] = await Promise.all([
|
|
171
|
+
client.provider.list(request),
|
|
172
|
+
client.model.list(request),
|
|
173
|
+
]);
|
|
174
|
+
const source = composeV2ProviderCatalogSource(providersResult?.data, modelsResult?.data);
|
|
175
|
+
if (!source) {
|
|
137
176
|
throw new Error('OpenCode provider catalog is unavailable');
|
|
138
177
|
}
|
|
139
|
-
const catalog = toSmallModelCatalog(
|
|
178
|
+
const catalog = toSmallModelCatalog(source);
|
|
140
179
|
if (!catalog) {
|
|
141
180
|
throw new Error('OpenCode provider catalog returned an unexpected payload');
|
|
142
181
|
}
|
|
@@ -144,8 +183,8 @@ export function createModelCatalogLoader({
|
|
|
144
183
|
};
|
|
145
184
|
|
|
146
185
|
/**
|
|
147
|
-
* Load (or serve cached) directory-scoped catalog.
|
|
148
|
-
*
|
|
186
|
+
* Load (or serve cached) directory-scoped catalog. Failures are not cached
|
|
187
|
+
* and reject with statusCode 502 so callers never resolve against a guess.
|
|
149
188
|
*/
|
|
150
189
|
const getModelCatalog = async (directory) => {
|
|
151
190
|
const key = normalizeDirectoryKey(directory);
|
|
@@ -156,7 +195,7 @@ export function createModelCatalogLoader({
|
|
|
156
195
|
}
|
|
157
196
|
|
|
158
197
|
if (!bucket) {
|
|
159
|
-
bucket = { catalog: null, cachedAt: 0,
|
|
198
|
+
bucket = { catalog: null, cachedAt: 0, inflight: null };
|
|
160
199
|
buckets.set(key, bucket);
|
|
161
200
|
}
|
|
162
201
|
|
|
@@ -166,18 +205,12 @@ export function createModelCatalogLoader({
|
|
|
166
205
|
const catalog = await fetchOpenCodeCatalog(key || undefined);
|
|
167
206
|
bucket.catalog = catalog;
|
|
168
207
|
bucket.cachedAt = Date.now();
|
|
169
|
-
bucket.source = 'opencode';
|
|
170
208
|
return catalog;
|
|
171
209
|
} catch (error) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
error?.message || error,
|
|
210
|
+
throw Object.assign(
|
|
211
|
+
new Error(`OpenCode provider catalog is unavailable (${error?.message || error})`),
|
|
212
|
+
{ statusCode: 502 },
|
|
176
213
|
);
|
|
177
|
-
bucket.catalog = MINIMAL_FALLBACK_CATALOG;
|
|
178
|
-
bucket.cachedAt = Date.now();
|
|
179
|
-
bucket.source = 'fallback';
|
|
180
|
-
return MINIMAL_FALLBACK_CATALOG;
|
|
181
214
|
} finally {
|
|
182
215
|
bucket.inflight = null;
|
|
183
216
|
}
|
|
@@ -192,8 +225,5 @@ export function createModelCatalogLoader({
|
|
|
192
225
|
getCatalogProvider,
|
|
193
226
|
/** @internal test helpers */
|
|
194
227
|
_normalizeDirectoryKey: normalizeDirectoryKey,
|
|
195
|
-
_minimalFallbackCatalog: MINIMAL_FALLBACK_CATALOG,
|
|
196
228
|
};
|
|
197
229
|
}
|
|
198
|
-
|
|
199
|
-
export { MINIMAL_FALLBACK_CATALOG };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { createModelCatalogLoader, toSmallModelCatalog
|
|
2
|
+
import { createModelCatalogLoader, toSmallModelCatalog } from './catalog.js';
|
|
3
3
|
|
|
4
4
|
describe('toSmallModelCatalog', () => {
|
|
5
5
|
it('keeps family, release_date, limit, cost, model.api.url, and provider name', () => {
|
|
@@ -45,13 +45,15 @@ describe('toSmallModelCatalog', () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
describe('createModelCatalogLoader', () => {
|
|
48
|
-
it('
|
|
48
|
+
it('rejects with 502 when OpenCode is unreachable and does not cache the failure', async () => {
|
|
49
|
+
let calls = 0;
|
|
49
50
|
const loader = createModelCatalogLoader({
|
|
50
51
|
buildOpenCodeUrl: () => 'http://127.0.0.1:9/',
|
|
51
52
|
getOpenCodeAuthHeaders: () => ({}),
|
|
52
53
|
ttlMs: 30_000,
|
|
53
54
|
timeoutMs: 50,
|
|
54
55
|
fetchImpl: async () => {
|
|
56
|
+
calls += 1;
|
|
55
57
|
throw new Error('network down');
|
|
56
58
|
},
|
|
57
59
|
});
|
|
@@ -59,13 +61,113 @@ describe('createModelCatalogLoader', () => {
|
|
|
59
61
|
const a = loader.getModelCatalog('/proj');
|
|
60
62
|
const b = loader.getModelCatalog('/proj/');
|
|
61
63
|
// Single-flight + directory key normalization: same bucket for trailing slash.
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
const results = await Promise.allSettled([a, b]);
|
|
65
|
+
for (const result of results) {
|
|
66
|
+
expect(result.status).toBe('rejected');
|
|
67
|
+
expect(result.reason.statusCode).toBe(502);
|
|
68
|
+
expect(result.reason.message).toMatch(/provider catalog is unavailable/);
|
|
69
|
+
}
|
|
70
|
+
const callsAfterFirst = calls;
|
|
71
|
+
await expect(loader.getModelCatalog('/proj')).rejects.toMatchObject({ statusCode: 502 });
|
|
72
|
+
expect(calls).toBeGreaterThan(callsAfterFirst);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('forwards OpenCode auth headers through the fetch wrapper', async () => {
|
|
76
|
+
const seen = [];
|
|
77
|
+
const loader = createModelCatalogLoader({
|
|
78
|
+
buildOpenCodeUrl: () => 'http://127.0.0.1:4096/',
|
|
79
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic test' }),
|
|
80
|
+
fetchImpl: async (url, init) => {
|
|
81
|
+
seen.push(new Headers(init?.headers).get('authorization'));
|
|
82
|
+
return new Response(JSON.stringify({ data: [] }), {
|
|
83
|
+
status: 200,
|
|
84
|
+
headers: { 'content-type': 'application/json' },
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await loader.getModelCatalog('/proj');
|
|
90
|
+
expect(seen.length).toBeGreaterThan(0);
|
|
91
|
+
expect(seen.every((value) => value === 'Basic test')).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('composes official v2 provider.list + model.list into the small-model catalog', async () => {
|
|
95
|
+
const loader = createModelCatalogLoader({
|
|
96
|
+
buildOpenCodeUrl: () => 'http://127.0.0.1:4096/',
|
|
97
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic test' }),
|
|
98
|
+
fetchImpl: async (url) => {
|
|
99
|
+
const href = String(url);
|
|
100
|
+
const data = href.includes('/api/model')
|
|
101
|
+
? [{
|
|
102
|
+
id: 'gemini-2.5-flash',
|
|
103
|
+
modelID: 'gemini-2.5-flash',
|
|
104
|
+
providerID: 'google',
|
|
105
|
+
family: 'gemini-flash',
|
|
106
|
+
name: 'Gemini 2.5 Flash',
|
|
107
|
+
capabilities: {},
|
|
108
|
+
variants: [],
|
|
109
|
+
time: { released: Date.parse('2025-06-01T00:00:00Z') },
|
|
110
|
+
cost: [],
|
|
111
|
+
status: 'active',
|
|
112
|
+
enabled: true,
|
|
113
|
+
limit: { context: 1_000_000, output: 8192 },
|
|
114
|
+
api: { url: 'https://generativelanguage.googleapis.com' },
|
|
115
|
+
}]
|
|
116
|
+
: [{ id: 'google', name: 'Google', package: '@ai-sdk/google' }];
|
|
117
|
+
return new Response(JSON.stringify({
|
|
118
|
+
location: { directory: '/proj', project: { id: 'p', directory: '/proj', canonical: '/proj' } },
|
|
119
|
+
data,
|
|
120
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const catalog = await loader.getModelCatalog('/proj');
|
|
125
|
+
expect(catalog.google).toEqual({
|
|
126
|
+
id: 'google',
|
|
127
|
+
name: 'Google',
|
|
128
|
+
models: {
|
|
129
|
+
'gemini-2.5-flash': {
|
|
130
|
+
id: 'gemini-2.5-flash',
|
|
131
|
+
family: 'gemini-flash',
|
|
132
|
+
release_date: '2025-06-01',
|
|
133
|
+
limit: { context: 1_000_000, output: 8192 },
|
|
134
|
+
api: { url: 'https://generativelanguage.googleapis.com' },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('keys models by ModelInfo.id and keeps the base tier of array cost', async () => {
|
|
141
|
+
const loader = createModelCatalogLoader({
|
|
142
|
+
buildOpenCodeUrl: () => 'http://127.0.0.1:4096/',
|
|
143
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
144
|
+
fetchImpl: async (url) => {
|
|
145
|
+
const href = String(url);
|
|
146
|
+
const data = href.includes('/api/model')
|
|
147
|
+
? [{
|
|
148
|
+
id: 'gpt-5.4-mini',
|
|
149
|
+
modelID: 'internal-pack',
|
|
150
|
+
providerID: 'openai',
|
|
151
|
+
name: 'GPT-5.4 Mini',
|
|
152
|
+
cost: [
|
|
153
|
+
{ tier: { type: 'context', size: 200000 }, input: 9, output: 9 },
|
|
154
|
+
{ input: 1, output: 2 },
|
|
155
|
+
],
|
|
156
|
+
}]
|
|
157
|
+
: [{ id: 'openai', name: 'OpenAI', package: '@ai-sdk/openai' }];
|
|
158
|
+
return new Response(JSON.stringify({ data }), {
|
|
159
|
+
status: 200,
|
|
160
|
+
headers: { 'content-type': 'application/json' },
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const catalog = await loader.getModelCatalog('/proj');
|
|
166
|
+
expect(catalog.openai?.models?.['gpt-5.4-mini']).toEqual({
|
|
167
|
+
id: 'gpt-5.4-mini',
|
|
168
|
+
cost: { input: 1, output: 2 },
|
|
169
|
+
});
|
|
170
|
+
expect(catalog.openai?.models?.['internal-pack']).toBeUndefined();
|
|
69
171
|
});
|
|
70
172
|
|
|
71
173
|
it('normalizes directory keys so trailing slashes share a bucket', () => {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const TEST_TIMEOUT_MS = 15_000;
|
|
2
2
|
const MODELS_TIMEOUT_MS = 8_000;
|
|
3
|
+
const GENERATE_TIMEOUT_MS = 60_000;
|
|
4
|
+
// Generous default: thinking models that can't be switched off spend part of
|
|
5
|
+
// this budget on reasoning before the actual answer.
|
|
6
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 4_000;
|
|
3
7
|
|
|
4
8
|
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
9
|
|
|
@@ -96,6 +100,70 @@ export async function testCustomSummaryApi({
|
|
|
96
100
|
}
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Summary AI custom mode: one non-streaming chat completion against the
|
|
105
|
+
* user's own OpenAI-compatible endpoint. The token never leaves this process.
|
|
106
|
+
*/
|
|
107
|
+
export async function generateCustomSummaryText({
|
|
108
|
+
baseURL,
|
|
109
|
+
apiToken,
|
|
110
|
+
modelID,
|
|
111
|
+
prompt,
|
|
112
|
+
system,
|
|
113
|
+
maxOutputTokens,
|
|
114
|
+
fetchImpl = globalThis.fetch.bind(globalThis),
|
|
115
|
+
}) {
|
|
116
|
+
const parsedBaseURL = parseCustomApiBaseURL(baseURL);
|
|
117
|
+
if (!parsedBaseURL) {
|
|
118
|
+
throw Object.assign(new Error('Custom summary API Base URL is invalid'), { statusCode: 400 });
|
|
119
|
+
}
|
|
120
|
+
const response = await fetchImpl(`${parsedBaseURL}/chat/completions`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
Accept: 'application/json',
|
|
125
|
+
Authorization: `Bearer ${apiToken}`,
|
|
126
|
+
},
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
model: modelID,
|
|
129
|
+
messages: [
|
|
130
|
+
...(system ? [{ role: 'system', content: system }] : []),
|
|
131
|
+
{ role: 'user', content: prompt },
|
|
132
|
+
],
|
|
133
|
+
max_tokens: Number(maxOutputTokens) > 0 ? Number(maxOutputTokens) : DEFAULT_MAX_OUTPUT_TOKENS,
|
|
134
|
+
stream: false,
|
|
135
|
+
}),
|
|
136
|
+
signal: AbortSignal.timeout(GENERATE_TIMEOUT_MS),
|
|
137
|
+
});
|
|
138
|
+
const bodyText = await response.text().catch(() => '');
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const snippet = bodyText ? `: ${bodyText.slice(0, 300)}` : '';
|
|
141
|
+
throw new Error(`Custom summary API request failed with ${response.status}${snippet}`);
|
|
142
|
+
}
|
|
143
|
+
const payload = parseJson(bodyText);
|
|
144
|
+
const message = payload?.choices?.[0]?.message;
|
|
145
|
+
// Providers disagree on the content shape: plain string, an array of typed
|
|
146
|
+
// parts, or (thinking models) empty content with the budget spent on
|
|
147
|
+
// reasoning_content.
|
|
148
|
+
let text = '';
|
|
149
|
+
if (typeof message?.content === 'string') {
|
|
150
|
+
text = message.content;
|
|
151
|
+
} else if (Array.isArray(message?.content)) {
|
|
152
|
+
text = message.content.map((part) => (typeof part?.text === 'string' ? part.text : '')).join('');
|
|
153
|
+
}
|
|
154
|
+
if (!text.trim() && typeof message?.reasoning_content === 'string' && message.reasoning_content.trim()) {
|
|
155
|
+
const finishReason = payload?.choices?.[0]?.finish_reason;
|
|
156
|
+
throw new Error(
|
|
157
|
+
'Custom summary API spent the output budget on reasoning and returned no answer'
|
|
158
|
+
+ (finishReason ? ` (finish_reason: ${finishReason})` : ''),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (!text.trim()) {
|
|
162
|
+
throw new Error('Custom summary API returned no message content');
|
|
163
|
+
}
|
|
164
|
+
return text;
|
|
165
|
+
}
|
|
166
|
+
|
|
99
167
|
export async function listCustomSummaryModels({
|
|
100
168
|
baseURL,
|
|
101
169
|
apiToken,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
classifyCustomSummaryApiFailure,
|
|
4
|
+
generateCustomSummaryText,
|
|
4
5
|
listCustomSummaryModels,
|
|
5
6
|
parseCustomApiBaseURL,
|
|
6
7
|
testCustomSummaryApi,
|
|
@@ -99,3 +100,66 @@ describe('listCustomSummaryModels', () => {
|
|
|
99
100
|
})).toEqual([]);
|
|
100
101
|
});
|
|
101
102
|
});
|
|
103
|
+
|
|
104
|
+
describe('generateCustomSummaryText', () => {
|
|
105
|
+
const completion = (message, extra = {}) => new Response(JSON.stringify({
|
|
106
|
+
choices: [{ message, ...extra }],
|
|
107
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
108
|
+
|
|
109
|
+
it('posts an OpenAI-compatible chat completion and returns the text', async () => {
|
|
110
|
+
const fetchImpl = vi.fn(async () => completion({ role: 'assistant', content: 'feat: add retries' }));
|
|
111
|
+
const text = await generateCustomSummaryText({
|
|
112
|
+
baseURL: 'https://summary.example.test/v1/',
|
|
113
|
+
apiToken: 'token',
|
|
114
|
+
modelID: 'summary-model',
|
|
115
|
+
prompt: 'Diff',
|
|
116
|
+
system: 'Return a commit subject.',
|
|
117
|
+
maxOutputTokens: 64,
|
|
118
|
+
fetchImpl,
|
|
119
|
+
});
|
|
120
|
+
expect(text).toBe('feat: add retries');
|
|
121
|
+
const [url, init] = fetchImpl.mock.calls[0];
|
|
122
|
+
expect(url).toBe('https://summary.example.test/v1/chat/completions');
|
|
123
|
+
expect(init.method).toBe('POST');
|
|
124
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
125
|
+
model: 'summary-model',
|
|
126
|
+
messages: [
|
|
127
|
+
{ role: 'system', content: 'Return a commit subject.' },
|
|
128
|
+
{ role: 'user', content: 'Diff' },
|
|
129
|
+
],
|
|
130
|
+
max_tokens: 64,
|
|
131
|
+
stream: false,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('joins array content parts', async () => {
|
|
136
|
+
const fetchImpl = vi.fn(async () => completion({ content: [{ type: 'text', text: 'a' }, { type: 'text', text: 'b' }] }));
|
|
137
|
+
await expect(generateCustomSummaryText({
|
|
138
|
+
baseURL: 'https://summary.example.test/v1', apiToken: 't', modelID: 'm', prompt: 'p', fetchImpl,
|
|
139
|
+
})).resolves.toBe('ab');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('rejects invalid base URLs before any request', async () => {
|
|
143
|
+
const fetchImpl = vi.fn();
|
|
144
|
+
await expect(generateCustomSummaryText({
|
|
145
|
+
baseURL: 'ftp://nope', apiToken: 't', modelID: 'm', prompt: 'p', fetchImpl,
|
|
146
|
+
})).rejects.toMatchObject({ statusCode: 400 });
|
|
147
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('surfaces HTTP failures, reasoning-only replies, and empty content as errors', async () => {
|
|
151
|
+
const input = { baseURL: 'https://summary.example.test/v1', apiToken: 't', modelID: 'm', prompt: 'p' };
|
|
152
|
+
await expect(generateCustomSummaryText({
|
|
153
|
+
...input,
|
|
154
|
+
fetchImpl: async () => new Response('rate limited', { status: 429 }),
|
|
155
|
+
})).rejects.toThrow(/failed with 429/);
|
|
156
|
+
await expect(generateCustomSummaryText({
|
|
157
|
+
...input,
|
|
158
|
+
fetchImpl: async () => completion({ content: '', reasoning_content: 'thinking…' }, { finish_reason: 'length' }),
|
|
159
|
+
})).rejects.toThrow(/reasoning.*finish_reason: length/);
|
|
160
|
+
await expect(generateCustomSummaryText({
|
|
161
|
+
...input,
|
|
162
|
+
fetchImpl: async () => completion({ content: '' }),
|
|
163
|
+
})).rejects.toThrow(/no message content/);
|
|
164
|
+
});
|
|
165
|
+
});
|