@openchambery/web 1.19.21-beta.1 → 2.0.0-beta.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 +13 -11
- 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 +165 -0
- package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
- package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
- package/dist/assets/{DiagramView-Bh06xinl.js → DiagramView-xuSJTnv7.js} +1 -1
- package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
- package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
- package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-f2agCc4q.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-ChD4WD2v.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
- package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
- package/dist/assets/{MobileShareBridge-D7SG0ueZ.js → MobileShareBridge-BQlP4J-F.js} +1 -1
- package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
- package/dist/assets/{MultiRunWindow-CjCw1X4n.js → MultiRunWindow-ChZGob0v.js} +1 -1
- package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
- package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
- package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
- package/dist/assets/{SettingsWindow-C2DgbXJ3.js → SettingsWindow-DfnsFa8y.js} +1 -1
- package/dist/assets/{TerminalView-lubDPKMs.js → TerminalView-Dh9MWCpS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-CzgW3-Cb.js → ToolOutputDialog-BejmjrRk.js} +1 -1
- package/dist/assets/{appThemeRegistry-BKc-qXbQ.js → appThemeRegistry-COGfxTFV.js} +1 -1
- package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
- package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
- package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
- package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
- package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
- package/dist/assets/es-c52LTHHe.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-BTkP7aYI.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
- package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
- package/dist/assets/gitApi-ButwzQ0N.js +9 -0
- package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
- package/dist/assets/ja-C8Ikun3Z.js +15 -0
- package/dist/assets/ko-ClFys5o2.js +15 -0
- package/dist/assets/main-CCE_4y5F.js +2 -0
- package/dist/assets/main-Dn9rpsgl.js +169 -0
- package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
- package/dist/assets/miniChat-BpjxVqgg.js +2 -0
- package/dist/assets/mobile-nMJCaNq1.js +2 -0
- package/dist/assets/multirun-BvSG94mW.js +4 -0
- package/dist/assets/pl-CnaKWN5F.js +15 -0
- package/dist/assets/platform-CWTIp02q.js +4 -0
- package/dist/assets/projectMeta-DOe49dM_.js +3 -0
- package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
- package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
- package/dist/assets/{runtimeConfig-PpnZiwcW.js → runtimeConfig-BMvqa5we.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BGjzNuQl.js → runtimeEndpointReset-B6BxhvR0.js} +6 -6
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +2 -0
- package/dist/assets/{sessionLookup-CbbzalcH.js → sessionLookup-gh-hVRIc.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
- package/dist/assets/src-BzX5UI-c.css +2 -0
- package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
- package/dist/assets/textarea-3XR8sUfy.js +1 -0
- package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
- package/dist/assets/uk-CaX0iqkB.js +15 -0
- package/dist/assets/{useAppFontEffects-BJBcy-84.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +1 -0
- package/dist/assets/{useEffectiveDirectory-CzHynoi_.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
- package/dist/assets/{useSessionAutoCleanup-DzV9N6G-.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-Bi9ASKr_.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/zh-CN-20MYEjha.js +15 -0
- package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +2 -2
- package/server/index.js +107 -0
- package/server/lib/assistants/DOCUMENTATION.md +18 -18
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/projection-history.test.js +207 -0
- package/server/lib/assistants/routes.js +5 -5
- package/server/lib/assistants/service.js +493 -123
- package/server/lib/assistants/service.test.js +203 -133
- package/server/lib/conversations/DOCUMENTATION.md +4 -4
- package/server/lib/conversations/service.js +86 -57
- package/server/lib/conversations/service.test.js +116 -100
- package/server/lib/event-stream/global-hub.js +2 -1
- package/server/lib/llm/DOCUMENTATION.md +71 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +37 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +421 -450
- package/server/lib/llm/generate.test.js +553 -487
- package/server/lib/llm/routes.js +6 -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.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/opencode/DOCUMENTATION.md +28 -5
- package/server/lib/opencode/config-entity-routes.js +84 -13
- package/server/lib/opencode/config-entity-routes.test.js +41 -10
- package/server/lib/opencode/ensure-cli.js +239 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +193 -110
- package/server/lib/opencode/env-runtime.test.js +221 -17
- package/server/lib/opencode/feature-routes-runtime.js +37 -1
- package/server/lib/opencode/lifecycle.js +204 -70
- package/server/lib/opencode/lifecycle.test.js +376 -6
- package/server/lib/opencode/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +87 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
- package/server/lib/opencode/plugin-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- package/server/lib/opencode/proxy.js +153 -16
- package/server/lib/opencode/proxy.test.js +120 -0
- package/server/lib/opencode/routes.js +25 -32
- package/server/lib/opencode/routes.test.js +51 -52
- package/server/lib/opencode/server-utils-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +6 -7
- package/server/lib/opencode/skill-routes.test.js +36 -5
- 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 +49 -0
- package/server/lib/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -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 +331 -308
- package/server/lib/scheduled-tasks/runtime.test.js +530 -318
- package/server/lib/session-goal/DOCUMENTATION.md +65 -14
- 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 +231 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +46 -4
- package/server/lib/session-goal/runtime.test.js +62 -1
- package/server/lib/session-index/sync-runtime.js +20 -17
- package/server/lib/session-index/sync-runtime.test.js +9 -8
- package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
- package/server/lib/session-metadata/routes.js +94 -0
- package/server/lib/session-metadata/routes.test.js +111 -0
- package/server/lib/session-metadata/session-metadata-store.js +235 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
- 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 +204 -71
- package/server/lib/session-turn-pages/routes.test.js +285 -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/small-model/DOCUMENTATION.md +5 -4
- package/server/lib/small-model/catalog.js +56 -6
- package/server/lib/small-model/catalog.test.js +47 -0
- package/server/lib/small-model/index.js +3 -3
- package/server/lib/small-model/opencode-session.js +50 -206
- package/server/lib/small-model/opencode-session.test.js +58 -88
- package/server/opencode-proxy.test.js +61 -12
- package/dist/assets/AssistantView-BtGYjSW5.js +0 -3
- package/dist/assets/MarkdownRenderer-AwDKOCvD.js +0 -2
- package/dist/assets/MobileDetailNavigation-B6fGRYsg.js +0 -1
- package/dist/assets/MobileSurface-BY7eErHN.js +0 -2
- package/dist/assets/SettingsView-lcRi35Y3.js +0 -25
- package/dist/assets/es-rqMiRP7-.js +0 -15
- package/dist/assets/gitApi-CcIuekQ3.js +0 -7
- package/dist/assets/insertionBoundaries-BocNNP9Q.js +0 -1
- package/dist/assets/ja-Bgr8-dOg.js +0 -15
- package/dist/assets/ko-B_Gxh-0l.js +0 -15
- package/dist/assets/main-CFLmpNC5.js +0 -169
- package/dist/assets/main-Ckj_IpUy.js +0 -2
- package/dist/assets/miniChat-Dq1CTKNs.js +0 -2
- package/dist/assets/mobile-DlJ0Yr7h.js +0 -2
- package/dist/assets/multirun-BGUKIJys.js +0 -4
- package/dist/assets/pl-DW2XJGC4.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/projectMeta-Bft2t6GF.js +0 -3
- package/dist/assets/pt-BR-CIsanf4T.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-CTbXbB0Y.js +0 -2
- package/dist/assets/renderMobileApp-DLewUqoE.js +0 -28
- package/dist/assets/sessionChangeSummary-DiqwX3vZ.js +0 -2
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/textarea-DpK2Z0Gp.js +0 -1
- package/dist/assets/uk-BPmPEUYJ.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-3Bm0rXCQ.js +0 -1
- package/dist/assets/useMobileNavigationStore-BIyCYi_z.js +0 -413
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
|
@@ -5,103 +5,85 @@ only model path the Assistant harness (`pi-agent-core` `streamFn`) talks to.
|
|
|
5
5
|
|
|
6
6
|
## Contract
|
|
7
7
|
|
|
8
|
-
- `GET /api/openchamber/llm/models` —
|
|
9
|
-
from
|
|
10
|
-
|
|
8
|
+
- `GET /api/openchamber/llm/models` — available `{providerID, modelID}` catalog
|
|
9
|
+
from official `@opencode/client` `provider.list` + `model.list`
|
|
10
|
+
(`{ location, data }`). `ModelInfo.id` is the external modelID used in
|
|
11
|
+
generate/session refs (`ModelInfo.modelID` is a separate internal field).
|
|
12
|
+
Vision prefers `capabilities.input.image === true` (SDK object shape), then
|
|
13
|
+
v2 list `capabilities.input` containing `"image"`, then legacy
|
|
14
|
+
modalities/input/attachment. Catalog loads use a bounded AbortSignal
|
|
15
|
+
(default 8s) so a pending provider call cannot stall a contact lane.
|
|
11
16
|
- `POST /api/openchamber/llm/chat/completions` — `{ model, messages, stream?,
|
|
12
17
|
providerID?, modelID?, variant? }` → OpenAI `chat.completion` JSON.
|
|
13
|
-
This **public** gateway is **non-streaming**.
|
|
14
|
-
(`POST /generate` when present, otherwise throwaway `session.promptAsync`
|
|
15
|
-
plus idle wait) returns a full assistant turn. `stream: true` is rejected with
|
|
18
|
+
This **public** gateway is **non-streaming**. `stream: true` is rejected with
|
|
16
19
|
`validation_error` (HTTP 400). Do not emit fake SSE after the fact on this
|
|
17
|
-
route
|
|
18
|
-
The contact UI may stream later via a **server-internal** path only (below).
|
|
20
|
+
route. The contact UI may stream later via a **server-internal** path only.
|
|
19
21
|
|
|
20
22
|
`model` may be `providerID/modelID` or a bare `modelID` paired with `providerID`.
|
|
21
23
|
|
|
22
24
|
`variant` is an optional OpenCode model variant string. Null, omitted, or empty
|
|
23
25
|
values preserve the provider default. Other types fail validation before a model
|
|
24
|
-
call. A selected variant is forwarded unchanged
|
|
25
|
-
|
|
26
|
+
call. A selected variant is forwarded unchanged on `generate.text` and on the
|
|
27
|
+
attachment-session `model` ref. OpenCode owns provider-specific variant
|
|
26
28
|
semantics. Variant selection changes inference configuration only; completion
|
|
27
29
|
output continues to include text parts and keeps reasoning parts private.
|
|
28
30
|
|
|
29
|
-
## Internals (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
cannot skip a newer agent body. Concurrent warm refreshes serialize through one
|
|
76
|
-
write chain (last committed body wins; no interleaved partial writes).
|
|
77
|
-
`stopLlmTempDirectory` clears the singleton for shutdown/tests.
|
|
31
|
+
## Internals (official `@opencode/client`)
|
|
32
|
+
|
|
33
|
+
Credentials stay in OpenCode. This module does not read `auth.json`, does not
|
|
34
|
+
call Anthropic/OpenAI/plugin SDKs, and does not use the `openai` npm package.
|
|
35
|
+
|
|
36
|
+
### Text path (no vision image bytes)
|
|
37
|
+
|
|
38
|
+
`client.generate.text({ location?, prompt, model: { id, providerID, variant? } },
|
|
39
|
+
{ signal })` → `{ text }`. No session. System text is prepended into `prompt`.
|
|
40
|
+
`onTextDelta` is skipped honestly (no fake typewriter).
|
|
41
|
+
|
|
42
|
+
### Attachment / vision path
|
|
43
|
+
|
|
44
|
+
When the catalog marks the model vision-capable and the request includes image
|
|
45
|
+
data-URL file parts:
|
|
46
|
+
|
|
47
|
+
1. Ensure the throwaway temp directory agent markdown (`openchamber-llm`,
|
|
48
|
+
permissions array ending in `action:* resource:* effect:deny`).
|
|
49
|
+
2. `agent.get({ agentID: 'openchamber-llm', location })` and require the **final**
|
|
50
|
+
permissions rule to be deny-all. Failure → `llm_attachment_generation_unavailable`
|
|
51
|
+
**before** any `session.prompt`. Title is never treated as permission isolation.
|
|
52
|
+
3. `session.create({ location, agent, model: { id, providerID, variant? } })`
|
|
53
|
+
(bare session id).
|
|
54
|
+
4. Optional `session.instructions.entry.put` for the system prompt.
|
|
55
|
+
5. Subscribe for `session.text.delta` (`data.sessionID` / `assistantMessageID` /
|
|
56
|
+
`ordinal` / `delta`) before prompt; unsubscribe in `finally`.
|
|
57
|
+
6. `session.prompt({ sessionID, text, files: [{ uri, name? }], delivery })`.
|
|
58
|
+
7. `session.wait` then `message.list({ order: 'desc' })`; verify newest assistant
|
|
59
|
+
`finish` / `error`; extract text content.
|
|
60
|
+
8. On timeout/abort: `session.interrupt({ continue: false })` and always
|
|
61
|
+
`session.remove` in `finally`.
|
|
62
|
+
|
|
63
|
+
Non-vision models keep the `[image: …]` description strategy and never forward
|
|
64
|
+
image bytes. Data URLs are strictly validated; a single attachment is capped at
|
|
65
|
+
20 MiB (plus existing inline text-file limits). An external/temp workspace that
|
|
66
|
+
OpenCode cannot see fails explicitly (`llm_attachment_generation_unavailable`).
|
|
67
|
+
|
|
68
|
+
`ensureLlmTempDirectory` keeps one process-wide throwaway root (`openchamber-llm-…`).
|
|
69
|
+
Concurrent first callers share a single `mkdtemp` inflight; every caller still
|
|
70
|
+
writes/refreshes `.opencode/agent/<name>.md` after settle so a mid-flight ensure
|
|
71
|
+
cannot skip a newer agent body. Concurrent warm refreshes serialize through one
|
|
72
|
+
write chain (last committed body wins; no interleaved partial writes).
|
|
73
|
+
`stopLlmTempDirectory` clears the singleton for shutdown/tests.
|
|
74
|
+
|
|
75
|
+
There is no `/generate` probe, no `tool.ids` deny map, and no `promptAsync` path.
|
|
76
|
+
There is no `config.providers` catalog path.
|
|
78
77
|
|
|
79
78
|
### Internal token callback (in-process only)
|
|
80
79
|
|
|
81
80
|
`generateOpenCodeText` / `createChatCompletion` accept optional
|
|
82
|
-
`onTextDelta(text: string)` and `globalEventHub
|
|
83
|
-
`globalMessageStreamHub` / `subscribeEvent` surface).
|
|
84
|
-
|
|
85
|
-
- **Throwaway session path:** after archive, before `promptAsync`, subscribe to
|
|
86
|
-
the hub. Forward real OpenCode `message.part.delta` events whose
|
|
87
|
-
`properties.sessionID` matches this throwaway session, `field` is `text`
|
|
88
|
-
(or omitted), and `messageID` matches the first assistant message locked for
|
|
89
|
-
that generate. Still wait for idle and return the **full** text (strip/parse
|
|
90
|
-
needs the complete string). Unsubscribe in `finally` so failed generates
|
|
91
|
-
cannot leak listeners.
|
|
92
|
-
- **Sessionless `/generate` JSON path:** cannot emit live deltas — skip
|
|
93
|
-
`onTextDelta` honestly (no fake typewriter of the completed string).
|
|
94
|
-
- **HTTP `POST .../chat/completions`:** does not pass these options and still
|
|
95
|
-
rejects `stream: true`. Token deltas never leave the server as SSE on that
|
|
96
|
-
route.
|
|
81
|
+
`onTextDelta(text: string)` and `globalEventHub`.
|
|
97
82
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Credentials stay in OpenCode. This module does not read `auth.json`, does not
|
|
104
|
-
call Anthropic/OpenAI/plugin SDKs, and does not use the `openai` npm package.
|
|
83
|
+
- **Attachment session path:** real `session.text.delta` tokens for this session.
|
|
84
|
+
- **`generate.text` path:** cannot emit live deltas — skip `onTextDelta`.
|
|
85
|
+
- **HTTP `POST .../chat/completions`:** does not pass these options and still
|
|
86
|
+
rejects `stream: true`.
|
|
105
87
|
|
|
106
88
|
No connected provider → `no_provider` (HTTP 400). Upstream failure is never
|
|
107
89
|
an empty success. The 502 body includes `{ error, message }`.
|
|
@@ -117,6 +99,11 @@ application tool catalog and actual results.
|
|
|
117
99
|
The Assistant contact harness owns system prompt, OpenChamber transcript,
|
|
118
100
|
bubble splitting, and OpenChamber API tools (`assign_session`). Those tools
|
|
119
101
|
deliver through contact **cards**, not this completions payload. The gateway
|
|
120
|
-
stays a text generator: OpenCode coding tools stay denied
|
|
121
|
-
|
|
122
|
-
|
|
102
|
+
stays a text generator: OpenCode coding tools stay denied on the attachment path
|
|
103
|
+
via the verified agent permissions ruleset.
|
|
104
|
+
|
|
105
|
+
Internal completion/generate calls accept an optional AbortSignal from the
|
|
106
|
+
contact continuation. It is combined with the generator deadline and passed to
|
|
107
|
+
upstream requests. Cancellation still runs throwaway-session cleanup
|
|
108
|
+
(`interrupt` + `remove`); it must not turn a late model response into a new
|
|
109
|
+
tool operation.
|
|
@@ -7,8 +7,10 @@ const listIncludesImage = (value) => Array.isArray(value)
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Vision capability projection.
|
|
10
|
-
* Prefer authoritative OpenCode
|
|
11
|
-
*
|
|
10
|
+
* Prefer authoritative OpenCode Model.capabilities (`input.image` boolean /
|
|
11
|
+
* `attachment`, or v2 list `input` containing `"image"`), then fall back to
|
|
12
|
+
* legacy modalities/input/attachment shapes used by older fixtures.
|
|
13
|
+
* Do not treat model titles or unrelated flags as vision isolation.
|
|
12
14
|
*/
|
|
13
15
|
export function modelAcceptsImages(model) {
|
|
14
16
|
if (!isRecord(model)) return false;
|
|
@@ -16,6 +18,7 @@ export function modelAcceptsImages(model) {
|
|
|
16
18
|
if (isRecord(model.capabilities.input) && typeof model.capabilities.input.image === 'boolean') {
|
|
17
19
|
return model.capabilities.input.image === true;
|
|
18
20
|
}
|
|
21
|
+
if (listIncludesImage(model.capabilities.input)) return true;
|
|
19
22
|
if (model.capabilities.attachment === true) return true;
|
|
20
23
|
}
|
|
21
24
|
if (isRecord(model.modalities) && listIncludesImage(model.modalities.input)) return true;
|
|
@@ -24,49 +27,76 @@ export function modelAcceptsImages(model) {
|
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
30
|
+
* Project official v2 provider.list + model.list into the LLM gateway catalog.
|
|
31
|
+
*
|
|
32
|
+
* ModelInfo.id is the external modelID used in generate/session model refs.
|
|
33
|
+
* ModelInfo.modelID is a separate internal field and must not be preferred.
|
|
29
34
|
*
|
|
30
35
|
* @param {{
|
|
31
|
-
* connected?: unknown,
|
|
32
36
|
* providers?: unknown,
|
|
37
|
+
* models?: unknown,
|
|
33
38
|
* }} source
|
|
34
39
|
*/
|
|
35
40
|
export function projectConnectedModels(source) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
41
|
-
const models = [];
|
|
42
|
-
const providers = [];
|
|
43
|
-
const list = Array.isArray(source?.providers) ? source.providers : [];
|
|
44
|
-
for (const provider of list) {
|
|
41
|
+
const providersIn = Array.isArray(source?.providers) ? source.providers : [];
|
|
42
|
+
const modelsIn = Array.isArray(source?.models) ? source.models : [];
|
|
43
|
+
|
|
44
|
+
const providerMeta = new Map();
|
|
45
|
+
for (const provider of providersIn) {
|
|
45
46
|
if (!isRecord(provider)) continue;
|
|
46
47
|
const providerID = stringID(provider.id);
|
|
47
|
-
if (!providerID
|
|
48
|
-
|
|
49
|
-
const rawModels = isRecord(provider.models) ? Object.values(provider.models) : [];
|
|
50
|
-
for (const model of rawModels) {
|
|
51
|
-
if (!isRecord(model)) continue;
|
|
52
|
-
const modelID = stringID(model.id);
|
|
53
|
-
if (!modelID) continue;
|
|
54
|
-
const entry = {
|
|
55
|
-
providerID,
|
|
56
|
-
modelID,
|
|
57
|
-
name: stringID(model.name) || modelID,
|
|
58
|
-
acceptsImages: modelAcceptsImages(model),
|
|
59
|
-
};
|
|
60
|
-
models.push(entry);
|
|
61
|
-
providerModels.push({ id: modelID, name: entry.name });
|
|
62
|
-
}
|
|
63
|
-
providers.push({
|
|
48
|
+
if (!providerID) continue;
|
|
49
|
+
providerMeta.set(providerID, {
|
|
64
50
|
id: providerID,
|
|
65
51
|
name: stringID(provider.name) || providerID,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const models = [];
|
|
56
|
+
const modelsByProvider = new Map();
|
|
57
|
+
for (const model of modelsIn) {
|
|
58
|
+
if (!isRecord(model)) continue;
|
|
59
|
+
const providerID = stringID(model.providerID);
|
|
60
|
+
// External model id for generate.text / session model refs.
|
|
61
|
+
const modelID = stringID(model.id);
|
|
62
|
+
if (!providerID || !modelID) continue;
|
|
63
|
+
if (providerMeta.size > 0 && !providerMeta.has(providerID)) continue;
|
|
64
|
+
const name = stringID(model.name) || modelID;
|
|
65
|
+
const entry = {
|
|
66
|
+
providerID,
|
|
67
|
+
modelID,
|
|
68
|
+
name,
|
|
69
|
+
acceptsImages: modelAcceptsImages(model),
|
|
70
|
+
};
|
|
71
|
+
models.push(entry);
|
|
72
|
+
if (!modelsByProvider.has(providerID)) modelsByProvider.set(providerID, []);
|
|
73
|
+
modelsByProvider.get(providerID).push({ id: modelID, name });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const connected = [];
|
|
77
|
+
const providers = [];
|
|
78
|
+
const providerIDs = providerMeta.size > 0
|
|
79
|
+
? [...providerMeta.keys()]
|
|
80
|
+
: [...modelsByProvider.keys()];
|
|
81
|
+
for (const providerID of providerIDs) {
|
|
82
|
+
const meta = providerMeta.get(providerID) || { id: providerID, name: providerID };
|
|
83
|
+
const providerModels = modelsByProvider.get(providerID) || [];
|
|
84
|
+
if (providerModels.length === 0 && providerMeta.size > 0) {
|
|
85
|
+
// Provider listed with zero models is still "connected" but contributes no entries.
|
|
86
|
+
connected.push(providerID);
|
|
87
|
+
providers.push({ id: meta.id, name: meta.name, models: [] });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (providerModels.length === 0) continue;
|
|
91
|
+
connected.push(providerID);
|
|
92
|
+
providers.push({
|
|
93
|
+
id: meta.id,
|
|
94
|
+
name: meta.name,
|
|
66
95
|
models: providerModels,
|
|
67
96
|
});
|
|
68
97
|
}
|
|
69
|
-
|
|
98
|
+
|
|
99
|
+
return { connected, providers, models };
|
|
70
100
|
}
|
|
71
101
|
|
|
72
102
|
export function parseModelRef(model, providerID, modelID) {
|
|
@@ -92,7 +122,7 @@ export function isConnectedModel(catalog, providerID, modelID) {
|
|
|
92
122
|
return catalog.models.some((entry) => entry.providerID === providerID && entry.modelID === modelID);
|
|
93
123
|
}
|
|
94
124
|
|
|
95
|
-
/** Bound provider/
|
|
125
|
+
/** Bound provider/model catalog loads so a pending SDK call cannot stall a contact lane. */
|
|
96
126
|
export const CONNECTED_CATALOG_TIMEOUT_MS = 8_000;
|
|
97
127
|
|
|
98
128
|
const isAbortError = (error) => (
|
|
@@ -101,18 +131,41 @@ const isAbortError = (error) => (
|
|
|
101
131
|
|| (typeof error?.message === 'string' && /aborted|timed out/i.test(error.message))
|
|
102
132
|
);
|
|
103
133
|
|
|
134
|
+
const resolveCatalogLocation = (options) => {
|
|
135
|
+
if (!options || typeof options !== 'object') return undefined;
|
|
136
|
+
if (isRecord(options.location)) return options.location;
|
|
137
|
+
if (options.directory || options.workspace) {
|
|
138
|
+
return {
|
|
139
|
+
...(options.directory ? { directory: options.directory } : {}),
|
|
140
|
+
...(options.workspace ? { workspace: options.workspace } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
};
|
|
145
|
+
|
|
104
146
|
/**
|
|
105
|
-
* Load the connected catalog from an
|
|
147
|
+
* Load the connected catalog from an official `@opencode/client`.
|
|
148
|
+
* Uses model.list + provider.list only (no config.providers).
|
|
106
149
|
* Failure is distinct from a successful empty catalog.
|
|
107
|
-
*
|
|
150
|
+
*
|
|
151
|
+
* Real client returns `{ location, data }` and throws declared JSON errors
|
|
152
|
+
* (no `{ error }` envelope). Uses a bounded AbortSignal (default 8s).
|
|
108
153
|
*
|
|
109
154
|
* @param {object} client
|
|
110
|
-
* @param {{
|
|
155
|
+
* @param {{
|
|
156
|
+
* location?: { directory?: string, workspace?: string },
|
|
157
|
+
* directory?: string,
|
|
158
|
+
* workspace?: string,
|
|
159
|
+
* signal?: AbortSignal,
|
|
160
|
+
* timeoutMs?: number,
|
|
161
|
+
* }} [options]
|
|
111
162
|
*/
|
|
112
163
|
export async function loadConnectedCatalog(client, options = {}) {
|
|
113
164
|
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
|
|
114
165
|
? Math.trunc(options.timeoutMs)
|
|
115
166
|
: CONNECTED_CATALOG_TIMEOUT_MS;
|
|
167
|
+
const location = resolveCatalogLocation(options);
|
|
168
|
+
const request = location ? { location } : undefined;
|
|
116
169
|
const parentSignal = options.signal;
|
|
117
170
|
const controller = new AbortController();
|
|
118
171
|
const onParentAbort = () => {
|
|
@@ -139,21 +192,34 @@ export async function loadConnectedCatalog(client, options = {}) {
|
|
|
139
192
|
|
|
140
193
|
const requestOptions = { signal: controller.signal };
|
|
141
194
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
195
|
+
let providersResult;
|
|
196
|
+
let modelsResult;
|
|
197
|
+
try {
|
|
198
|
+
[providersResult, modelsResult] = await Promise.all([
|
|
199
|
+
client.provider.list(request, requestOptions),
|
|
200
|
+
client.model.list(request, requestOptions),
|
|
201
|
+
]);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (controller.signal.aborted || isAbortError(error)) {
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
const upstream = new Error(
|
|
207
|
+
typeof error?.message === 'string' && error.message.trim()
|
|
208
|
+
? error.message
|
|
209
|
+
: 'OpenCode provider catalog is unavailable',
|
|
210
|
+
);
|
|
211
|
+
upstream.code = 'upstream_error';
|
|
212
|
+
upstream.cause = error;
|
|
213
|
+
throw upstream;
|
|
147
214
|
}
|
|
148
|
-
|
|
149
|
-
if (configured?.error || !isRecord(configured?.data)) {
|
|
215
|
+
if (!Array.isArray(providersResult?.data) || !Array.isArray(modelsResult?.data)) {
|
|
150
216
|
const error = new Error('OpenCode provider catalog is unavailable');
|
|
151
217
|
error.code = 'upstream_error';
|
|
152
218
|
throw error;
|
|
153
219
|
}
|
|
154
220
|
return projectConnectedModels({
|
|
155
|
-
|
|
156
|
-
|
|
221
|
+
providers: providersResult.data,
|
|
222
|
+
models: modelsResult.data,
|
|
157
223
|
});
|
|
158
224
|
} catch (error) {
|
|
159
225
|
if (controller.signal.aborted || isAbortError(error)) {
|