@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
|
@@ -76,62 +76,82 @@ describe('modelAcceptsImages', () => {
|
|
|
76
76
|
expect(modelAcceptsImages({ attachment: true })).toBe(true)
|
|
77
77
|
expect(modelAcceptsImages({ id: 'plain' })).toBe(false)
|
|
78
78
|
})
|
|
79
|
+
|
|
80
|
+
it('accepts v2 list-shaped capabilities.input containing image', () => {
|
|
81
|
+
expect(modelAcceptsImages({
|
|
82
|
+
capabilities: { tools: true, input: ['text', 'image'], output: ['text'] },
|
|
83
|
+
})).toBe(true)
|
|
84
|
+
expect(modelAcceptsImages({
|
|
85
|
+
capabilities: { tools: true, input: ['text'], output: ['text'] },
|
|
86
|
+
})).toBe(false)
|
|
87
|
+
})
|
|
79
88
|
})
|
|
80
89
|
|
|
81
90
|
describe('projectConnectedModels', () => {
|
|
82
|
-
it('projects
|
|
91
|
+
it('projects models from provider.list + model.list using ModelInfo.id as modelID', () => {
|
|
83
92
|
const catalog = projectConnectedModels({
|
|
84
|
-
connected: ['openai'],
|
|
85
93
|
providers: [
|
|
94
|
+
{ id: 'openai', name: 'OpenAI' },
|
|
95
|
+
{ id: 'anthropic', name: 'Anthropic' },
|
|
96
|
+
],
|
|
97
|
+
models: [
|
|
86
98
|
{
|
|
87
|
-
id: '
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
},
|
|
99
|
+
id: 'gpt-5.2',
|
|
100
|
+
modelID: 'internal-gpt-pack',
|
|
101
|
+
providerID: 'openai',
|
|
102
|
+
name: 'GPT-5.2',
|
|
103
|
+
capabilities: { tools: true, input: ['text'], output: ['text'] },
|
|
92
104
|
},
|
|
93
105
|
{
|
|
94
|
-
id: '
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
id: 'claude-sonnet-4',
|
|
107
|
+
modelID: 'internal-claude',
|
|
108
|
+
providerID: 'anthropic',
|
|
109
|
+
name: 'Claude',
|
|
110
|
+
capabilities: { tools: true, input: ['text'], output: ['text'] },
|
|
97
111
|
},
|
|
98
112
|
],
|
|
99
113
|
})
|
|
100
114
|
expect(catalog.models).toEqual([
|
|
101
115
|
{ providerID: 'openai', modelID: 'gpt-5.2', name: 'GPT-5.2', acceptsImages: false },
|
|
116
|
+
{ providerID: 'anthropic', modelID: 'claude-sonnet-4', name: 'Claude', acceptsImages: false },
|
|
102
117
|
])
|
|
118
|
+
expect(catalog.connected).toEqual(['openai', 'anthropic'])
|
|
103
119
|
})
|
|
104
120
|
|
|
105
|
-
it('marks vision from SDK capabilities and legacy modalities', () => {
|
|
121
|
+
it('marks vision from SDK capabilities, v2 list input, and legacy modalities', () => {
|
|
106
122
|
const catalog = projectConnectedModels({
|
|
107
|
-
connected: ['openai', 'xai'],
|
|
108
123
|
providers: [
|
|
124
|
+
{ id: 'openai', name: 'OpenAI' },
|
|
125
|
+
{ id: 'xai', name: 'xAI' },
|
|
126
|
+
],
|
|
127
|
+
models: [
|
|
109
128
|
{
|
|
110
|
-
id: '
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
},
|
|
129
|
+
id: 'gpt-4o',
|
|
130
|
+
modelID: 'pack-4o',
|
|
131
|
+
providerID: 'openai',
|
|
132
|
+
name: 'GPT-4o',
|
|
133
|
+
capabilities: { tools: true, input: ['text', 'image'], output: ['text'] },
|
|
116
134
|
},
|
|
117
135
|
{
|
|
118
|
-
id: '
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
},
|
|
136
|
+
id: 'deepseek-v4-flash',
|
|
137
|
+
modelID: 'pack-ds',
|
|
138
|
+
providerID: 'openai',
|
|
139
|
+
name: 'deepseek-v4-flash',
|
|
140
|
+
capabilities: { tools: true, input: ['text'], output: ['text'] },
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'grok-4.6',
|
|
144
|
+
modelID: 'pack-grok',
|
|
145
|
+
providerID: 'xai',
|
|
146
|
+
name: 'Grok 4.6',
|
|
147
|
+
capabilities: {
|
|
148
|
+
temperature: true,
|
|
149
|
+
reasoning: true,
|
|
150
|
+
attachment: true,
|
|
151
|
+
toolcall: true,
|
|
152
|
+
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
153
|
+
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
154
|
+
interleaved: false,
|
|
135
155
|
},
|
|
136
156
|
},
|
|
137
157
|
],
|
|
@@ -141,6 +161,17 @@ describe('projectConnectedModels', () => {
|
|
|
141
161
|
expect(catalog.models.find((model) => model.modelID === 'deepseek-v4-flash')?.acceptsImages).toBe(false)
|
|
142
162
|
expect(catalog.connected).toEqual(['openai', 'xai'])
|
|
143
163
|
})
|
|
164
|
+
|
|
165
|
+
it('ignores models whose provider is not in provider.list when providers are present', () => {
|
|
166
|
+
const catalog = projectConnectedModels({
|
|
167
|
+
providers: [{ id: 'openai', name: 'OpenAI' }],
|
|
168
|
+
models: [
|
|
169
|
+
{ id: 'gpt-5.2', providerID: 'openai', name: 'GPT-5.2', capabilities: { input: ['text'], output: ['text'], tools: false } },
|
|
170
|
+
{ id: 'other', providerID: 'missing', name: 'Other', capabilities: { input: ['text'], output: ['text'], tools: false } },
|
|
171
|
+
],
|
|
172
|
+
})
|
|
173
|
+
expect(catalog.models.map((m) => m.modelID)).toEqual(['gpt-5.2'])
|
|
174
|
+
})
|
|
144
175
|
})
|
|
145
176
|
|
|
146
177
|
describe('loadConnectedCatalog', () => {
|
|
@@ -148,42 +179,73 @@ describe('loadConnectedCatalog', () => {
|
|
|
148
179
|
vi.restoreAllMocks()
|
|
149
180
|
})
|
|
150
181
|
|
|
151
|
-
it('requires
|
|
182
|
+
it('requires provider.list and model.list data arrays', async () => {
|
|
152
183
|
await expect(loadConnectedCatalog({
|
|
153
|
-
provider: { list: async () => ({ data: {
|
|
154
|
-
|
|
184
|
+
provider: { list: async () => ({ location: {}, data: [{ id: 'openai', name: 'OpenAI' }] }) },
|
|
185
|
+
model: { list: async () => ({ location: {}, data: null }) },
|
|
155
186
|
})).rejects.toThrow(/provider catalog/)
|
|
156
187
|
})
|
|
157
188
|
|
|
158
|
-
it('
|
|
189
|
+
it('composes real client-shaped catalog responses', async () => {
|
|
190
|
+
const catalog = await loadConnectedCatalog({
|
|
191
|
+
provider: {
|
|
192
|
+
list: async () => ({
|
|
193
|
+
location: { directory: '/tmp' },
|
|
194
|
+
data: [{ id: 'openai', name: 'OpenAI' }],
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
model: {
|
|
198
|
+
list: async () => ({
|
|
199
|
+
location: { directory: '/tmp' },
|
|
200
|
+
data: [{
|
|
201
|
+
id: 'gpt-4o',
|
|
202
|
+
modelID: 'internal',
|
|
203
|
+
providerID: 'openai',
|
|
204
|
+
name: 'GPT-4o',
|
|
205
|
+
capabilities: { tools: true, input: ['text', 'image'], output: ['text'] },
|
|
206
|
+
}],
|
|
207
|
+
}),
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
expect(catalog.models).toEqual([
|
|
211
|
+
{ providerID: 'openai', modelID: 'gpt-4o', name: 'GPT-4o', acceptsImages: true },
|
|
212
|
+
])
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('surfaces thrown client errors as upstream_error', async () => {
|
|
216
|
+
await expect(loadConnectedCatalog({
|
|
217
|
+
provider: { list: async () => { throw new Error('boom') } },
|
|
218
|
+
model: { list: async () => ({ data: [] }) },
|
|
219
|
+
})).rejects.toMatchObject({ code: 'upstream_error', message: 'boom' })
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('passes AbortSignal to provider.list and model.list', async () => {
|
|
159
223
|
const signals = []
|
|
160
224
|
const catalog = await loadConnectedCatalog({
|
|
161
225
|
provider: {
|
|
162
226
|
list: async (_params, options) => {
|
|
163
227
|
signals.push(options?.signal)
|
|
164
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
location: {},
|
|
230
|
+
data: [{ id: 'xai', name: 'xAI' }],
|
|
231
|
+
}
|
|
165
232
|
},
|
|
166
233
|
},
|
|
167
|
-
|
|
168
|
-
|
|
234
|
+
model: {
|
|
235
|
+
list: async (_params, options) => {
|
|
169
236
|
signals.push(options?.signal)
|
|
170
237
|
return {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
}],
|
|
186
|
-
},
|
|
238
|
+
location: {},
|
|
239
|
+
data: [{
|
|
240
|
+
id: 'grok-4.6',
|
|
241
|
+
modelID: 'internal',
|
|
242
|
+
providerID: 'xai',
|
|
243
|
+
name: 'Grok 4.6',
|
|
244
|
+
capabilities: {
|
|
245
|
+
input: { text: true, image: true },
|
|
246
|
+
attachment: true,
|
|
247
|
+
},
|
|
248
|
+
}],
|
|
187
249
|
}
|
|
188
250
|
},
|
|
189
251
|
},
|
|
@@ -209,7 +271,7 @@ describe('loadConnectedCatalog', () => {
|
|
|
209
271
|
else options?.signal?.addEventListener('abort', onAbort, { once: true })
|
|
210
272
|
}),
|
|
211
273
|
},
|
|
212
|
-
|
|
274
|
+
model: { list: async () => ({ data: [] }) },
|
|
213
275
|
}, { timeoutMs: 30 })).rejects.toMatchObject({
|
|
214
276
|
code: 'upstream_error',
|
|
215
277
|
message: expect.stringMatching(/timed out|aborted/i),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isConnectedModel, loadConnectedCatalog, parseModelRef } from './catalog.js';
|
|
2
|
-
import { generateOpenCodeText } from './generate.js';
|
|
2
|
+
import { generateOpenCodeText, parseDataUrl, MAX_ATTACHMENT_BYTES } from './generate.js';
|
|
3
3
|
|
|
4
4
|
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
5
|
|
|
@@ -15,10 +15,29 @@ const normalizeCompletionFilePart = (part) => {
|
|
|
15
15
|
if (!isRecord(part) || part.type !== 'file') return null;
|
|
16
16
|
if (typeof part.mime !== 'string' || !part.mime.trim()) return null;
|
|
17
17
|
if (typeof part.url !== 'string' || !part.url.trim()) return null;
|
|
18
|
+
const url = part.url.trim();
|
|
19
|
+
if (!url.startsWith('data:')) {
|
|
20
|
+
throw new LlmError('validation_error', 400, 'file part url must be a data URL');
|
|
21
|
+
}
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = parseDataUrl(url);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error?.code === 'validation_error') {
|
|
27
|
+
throw new LlmError('validation_error', 400, error.message || 'attachment too large');
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
if (!parsed) {
|
|
32
|
+
throw new LlmError('validation_error', 400, 'file part data URL is malformed');
|
|
33
|
+
}
|
|
34
|
+
if (parsed.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
35
|
+
throw new LlmError('validation_error', 400, `Attachment exceeds ${MAX_ATTACHMENT_BYTES} byte limit`);
|
|
36
|
+
}
|
|
18
37
|
return {
|
|
19
38
|
type: 'file',
|
|
20
39
|
mime: part.mime.trim(),
|
|
21
|
-
url
|
|
40
|
+
url,
|
|
22
41
|
...(typeof part.filename === 'string' && part.filename.trim() ? { filename: part.filename.trim() } : {}),
|
|
23
42
|
};
|
|
24
43
|
};
|
|
@@ -57,15 +76,15 @@ export function toOpenAICompletion({ id, model, text, created = Math.floor(Date.
|
|
|
57
76
|
}],
|
|
58
77
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
59
78
|
};
|
|
60
|
-
}
|
|
79
|
+
};
|
|
61
80
|
|
|
62
81
|
/**
|
|
63
82
|
* OpenAI-shaped completions against OpenCode's already-connected providers.
|
|
64
83
|
*
|
|
65
84
|
* Public HTTP stays non-streaming (`stream: true` → validation_error).
|
|
66
85
|
* In-process callers (Assistant contact harness) may pass `onTextDelta` and
|
|
67
|
-
* `globalEventHub`; those are forwarded to generate for real
|
|
68
|
-
* `
|
|
86
|
+
* `globalEventHub`; those are forwarded to generate for real attachment-session
|
|
87
|
+
* `session.text.delta` tokens only — never as fake post-hoc SSE on this route.
|
|
69
88
|
*/
|
|
70
89
|
export async function createChatCompletion({
|
|
71
90
|
body,
|
|
@@ -75,16 +94,16 @@ export async function createChatCompletion({
|
|
|
75
94
|
loadCatalog = loadConnectedCatalog,
|
|
76
95
|
generateText = generateOpenCodeText,
|
|
77
96
|
ensureTempDirectory,
|
|
78
|
-
fetchImpl,
|
|
79
97
|
onTextDelta = null,
|
|
80
98
|
globalEventHub = null,
|
|
99
|
+
persistSessionMetadata = null,
|
|
100
|
+
onSystemSessionPersisted = null,
|
|
81
101
|
signal = null,
|
|
82
102
|
}) {
|
|
83
103
|
signal?.throwIfAborted();
|
|
84
104
|
if (!isRecord(body)) throw new LlmError('validation_error', 400, 'JSON body is required');
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// token-stream. Do not emit fake SSE after the fact.
|
|
105
|
+
// session.generate / attachment-session return a full turn. This HTTP gateway
|
|
106
|
+
// does not token-stream. Do not emit fake SSE after the fact.
|
|
88
107
|
// In-process onTextDelta (optional) is a separate internal callback path.
|
|
89
108
|
if (body.stream === true) {
|
|
90
109
|
throw new LlmError(
|
|
@@ -103,8 +122,8 @@ export async function createChatCompletion({
|
|
|
103
122
|
|
|
104
123
|
const client = clientFactory?.() ?? null;
|
|
105
124
|
const catalog = await loadCatalog(client ?? {
|
|
106
|
-
provider: { list: async () =>
|
|
107
|
-
|
|
125
|
+
provider: { list: async () => { throw new Error('no client'); } },
|
|
126
|
+
model: { list: async () => { throw new Error('no client'); } },
|
|
108
127
|
});
|
|
109
128
|
signal?.throwIfAborted();
|
|
110
129
|
if (!isConnectedModel(catalog, resolved.providerID, resolved.modelID)) {
|
|
@@ -124,7 +143,6 @@ export async function createChatCompletion({
|
|
|
124
143
|
modelID: resolved.modelID,
|
|
125
144
|
...(variant ? { variant } : {}),
|
|
126
145
|
messages,
|
|
127
|
-
fetchImpl,
|
|
128
146
|
clientFactory,
|
|
129
147
|
ensureTempDirectory,
|
|
130
148
|
forwardImageParts: Boolean(catalog.models.find((entry) => (
|
|
@@ -132,6 +150,8 @@ export async function createChatCompletion({
|
|
|
132
150
|
))?.acceptsImages),
|
|
133
151
|
onTextDelta,
|
|
134
152
|
globalEventHub,
|
|
153
|
+
persistSessionMetadata,
|
|
154
|
+
onSystemSessionPersisted,
|
|
135
155
|
signal,
|
|
136
156
|
});
|
|
137
157
|
} catch (error) {
|
|
@@ -139,6 +159,15 @@ export async function createChatCompletion({
|
|
|
139
159
|
if (error?.code === 'validation_error') {
|
|
140
160
|
throw new LlmError('validation_error', 400, error.message || 'validation_error');
|
|
141
161
|
}
|
|
162
|
+
if (error?.code === 'llm_attachment_generation_unavailable') {
|
|
163
|
+
throw new LlmError(
|
|
164
|
+
'llm_attachment_generation_unavailable',
|
|
165
|
+
502,
|
|
166
|
+
typeof error?.message === 'string' && error.message.trim()
|
|
167
|
+
? error.message
|
|
168
|
+
: 'LLM attachment generation is unavailable',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
142
171
|
throw new LlmError(
|
|
143
172
|
'upstream_error',
|
|
144
173
|
502,
|
|
@@ -52,7 +52,7 @@ describe('createChatCompletion', () => {
|
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
it('returns a non-stream completion from generateText', async () => {
|
|
55
|
-
const generateText = vi.fn(async () => ({ text: 'done', source: '
|
|
55
|
+
const generateText = vi.fn(async () => ({ text: 'done', source: 'session.generate' }))
|
|
56
56
|
const result = await createChatCompletion({
|
|
57
57
|
generateText,
|
|
58
58
|
loadCatalog: async () => ({
|
|
@@ -64,10 +64,11 @@ describe('createChatCompletion', () => {
|
|
|
64
64
|
expect(result.completion.choices[0].message.content).toBe('done')
|
|
65
65
|
expect(result.providerID).toBe('openai')
|
|
66
66
|
expect(result.modelID).toBe('gpt-5.2')
|
|
67
|
+
expect(result.source).toBe('session.generate')
|
|
67
68
|
})
|
|
68
69
|
|
|
69
70
|
it('forwards in-process onTextDelta and globalEventHub to generateText', async () => {
|
|
70
|
-
const generateText = vi.fn(async () => ({ text: 'streamed', source: '
|
|
71
|
+
const generateText = vi.fn(async () => ({ text: 'streamed', source: 'attachment-session' }))
|
|
71
72
|
const onTextDelta = vi.fn()
|
|
72
73
|
const signal = new AbortController().signal
|
|
73
74
|
const globalEventHub = { subscribeEvent: vi.fn() }
|
|
@@ -88,7 +89,7 @@ describe('createChatCompletion', () => {
|
|
|
88
89
|
})
|
|
89
90
|
|
|
90
91
|
it('forwards optional file parts on user messages', async () => {
|
|
91
|
-
const generateText = vi.fn(async () => ({ text: 'saw it', source: '
|
|
92
|
+
const generateText = vi.fn(async () => ({ text: 'saw it', source: 'attachment-session' }))
|
|
92
93
|
const image = { type: 'file', mime: 'image/png', url: 'data:image/png;base64,aa', filename: 'shot.png' }
|
|
93
94
|
await createChatCompletion({
|
|
94
95
|
generateText,
|
|
@@ -108,7 +109,7 @@ describe('createChatCompletion', () => {
|
|
|
108
109
|
})
|
|
109
110
|
|
|
110
111
|
it('asks generate to skip image bytes when the catalog model is not vision-capable', async () => {
|
|
111
|
-
const generateText = vi.fn(async () => ({ text: 'ok', source: '
|
|
112
|
+
const generateText = vi.fn(async () => ({ text: 'ok', source: 'session.generate' }))
|
|
112
113
|
await createChatCompletion({
|
|
113
114
|
generateText,
|
|
114
115
|
loadCatalog: async () => ({
|
|
@@ -120,6 +121,26 @@ describe('createChatCompletion', () => {
|
|
|
120
121
|
expect(generateText.mock.calls[0][0].forwardImageParts).toBe(false)
|
|
121
122
|
})
|
|
122
123
|
|
|
124
|
+
it('rejects malformed data URL file parts', async () => {
|
|
125
|
+
const generateText = vi.fn()
|
|
126
|
+
await expect(createChatCompletion({
|
|
127
|
+
generateText,
|
|
128
|
+
loadCatalog: async () => ({
|
|
129
|
+
models: [{ providerID: 'openai', modelID: 'gpt-5.2', acceptsImages: true }],
|
|
130
|
+
connected: ['openai'],
|
|
131
|
+
}),
|
|
132
|
+
body: {
|
|
133
|
+
model: 'openai/gpt-5.2',
|
|
134
|
+
messages: [{
|
|
135
|
+
role: 'user',
|
|
136
|
+
content: 'look',
|
|
137
|
+
parts: [{ type: 'file', mime: 'image/png', url: 'https://example.com/x.png' }],
|
|
138
|
+
}],
|
|
139
|
+
},
|
|
140
|
+
})).rejects.toMatchObject({ code: 'validation_error', statusCode: 400 })
|
|
141
|
+
expect(generateText).not.toHaveBeenCalled()
|
|
142
|
+
})
|
|
143
|
+
|
|
123
144
|
it('LlmError carries a stable code', () => {
|
|
124
145
|
expect(new LlmError('no_provider', 400, 'missing')).toMatchObject({
|
|
125
146
|
code: 'no_provider',
|
|
@@ -146,4 +167,24 @@ describe('createChatCompletion', () => {
|
|
|
146
167
|
message: 'model refused the request',
|
|
147
168
|
})
|
|
148
169
|
})
|
|
170
|
+
|
|
171
|
+
it('maps llm_attachment_generation_unavailable before treating it as generic upstream', async () => {
|
|
172
|
+
const generateText = vi.fn(async () => {
|
|
173
|
+
const error = new Error('deny-all missing')
|
|
174
|
+
error.code = 'llm_attachment_generation_unavailable'
|
|
175
|
+
throw error
|
|
176
|
+
})
|
|
177
|
+
await expect(createChatCompletion({
|
|
178
|
+
generateText,
|
|
179
|
+
loadCatalog: async () => ({
|
|
180
|
+
models: [{ providerID: 'openai', modelID: 'gpt-4o', acceptsImages: true }],
|
|
181
|
+
connected: ['openai'],
|
|
182
|
+
}),
|
|
183
|
+
body: { model: 'openai/gpt-4o', messages: [{ role: 'user', content: 'hi' }] },
|
|
184
|
+
})).rejects.toMatchObject({
|
|
185
|
+
code: 'llm_attachment_generation_unavailable',
|
|
186
|
+
statusCode: 502,
|
|
187
|
+
message: 'deny-all missing',
|
|
188
|
+
})
|
|
189
|
+
})
|
|
149
190
|
})
|