@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
|
@@ -39,7 +39,7 @@ const hasPartType = (parts, type) =>
|
|
|
39
39
|
Array.isArray(parts) && parts.some((part) => part && typeof part === 'object' && part.type === type);
|
|
40
40
|
|
|
41
41
|
const isHostedSessionDivider = (record) => {
|
|
42
|
-
const id = record?.info?.id;
|
|
42
|
+
const id = record?.info?.id ?? record?.id;
|
|
43
43
|
return typeof id === 'string' && id.startsWith(ASSISTANT_SESSION_DIVIDER_PREFIX);
|
|
44
44
|
};
|
|
45
45
|
|
|
@@ -53,6 +53,10 @@ export const isUserAuthoredTurnBoundary = (record) => {
|
|
|
53
53
|
if (!record || typeof record !== 'object') return false;
|
|
54
54
|
if (isHostedSessionDivider(record)) return false;
|
|
55
55
|
|
|
56
|
+
if (typeof record.type === 'string' && record.info == null) {
|
|
57
|
+
return record.type === 'user';
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
const info = record.info ?? {};
|
|
57
61
|
const role = typeof info.clientRole === 'string' ? info.clientRole : info.role;
|
|
58
62
|
if (role !== 'user') return false;
|
|
@@ -94,7 +98,7 @@ export const selectTurnRecords = (timeline, turnLimit) => {
|
|
|
94
98
|
};
|
|
95
99
|
|
|
96
100
|
const recordId = (record) => {
|
|
97
|
-
const id = record?.info?.id;
|
|
101
|
+
const id = record?.info?.id ?? record?.id;
|
|
98
102
|
return typeof id === 'string' && id.length > 0 ? id : null;
|
|
99
103
|
};
|
|
100
104
|
|
|
@@ -1241,3 +1241,37 @@ describe('createSessionTurnPageService', () => {
|
|
|
1241
1241
|
});
|
|
1242
1242
|
});
|
|
1243
1243
|
});
|
|
1244
|
+
|
|
1245
|
+
describe('SessionMessageInfo turn pages', () => {
|
|
1246
|
+
const v2User = (id, extra = {}) => ({ id, type: 'user', time: { created: 1 }, text: 'hi', ...extra });
|
|
1247
|
+
const v2Assistant = (id, extra = {}) => ({
|
|
1248
|
+
id,
|
|
1249
|
+
type: 'assistant',
|
|
1250
|
+
time: { created: 2, completed: 3 },
|
|
1251
|
+
content: [{ type: 'text', text: 'ok' }],
|
|
1252
|
+
...extra,
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it('counts SessionMessageInfo type=user as an authored boundary', () => {
|
|
1256
|
+
expect(isUserAuthoredTurnBoundary(v2User('msg_u1'))).toBe(true);
|
|
1257
|
+
expect(isUserAuthoredTurnBoundary(v2Assistant('msg_a1'))).toBe(false);
|
|
1258
|
+
expect(isUserAuthoredTurnBoundary({ id: 'msg_syn', type: 'synthetic', time: { created: 1 }, text: 'loop' })).toBe(false);
|
|
1259
|
+
expect(isUserAuthoredTurnBoundary({
|
|
1260
|
+
id: `oc_asst_session_divider:ses_old`,
|
|
1261
|
+
type: 'system',
|
|
1262
|
+
time: { created: 0 },
|
|
1263
|
+
})).toBe(false);
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
it('aggregates SessionMessageInfo pages by id and type', async () => {
|
|
1267
|
+
const fetchPage = vi.fn(async () => ({
|
|
1268
|
+
records: [v2User('msg_u1'), v2Assistant('msg_a1')],
|
|
1269
|
+
nextCursor: null,
|
|
1270
|
+
complete: true,
|
|
1271
|
+
}));
|
|
1272
|
+
const service = createSessionTurnPageService({ fetchPage });
|
|
1273
|
+
const result = await service.loadPage({ sessionID: 'ses_1', turns: 1 });
|
|
1274
|
+
expect(result).toMatchObject({ ok: true, turnCount: 1, complete: true });
|
|
1275
|
+
expect(result.records.map((entry) => entry.id)).toEqual(['msg_u1', 'msg_a1']);
|
|
1276
|
+
});
|
|
1277
|
+
});
|
|
@@ -24,10 +24,11 @@ every other runtime API.
|
|
|
24
24
|
tasks. Tests may call the factory with a mock `getModelCatalog`. Returns
|
|
25
25
|
`stop()` to remove the lazy temp directory used by the session path (wired
|
|
26
26
|
from `server/index.js` `stop()`).
|
|
27
|
-
- `opencode-session.js` —
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
- `opencode-session.js` — fallback for non-dedicated providers. Lazily prepares
|
|
28
|
+
`mkdtemp('openchamber-smallmodel-')` with a hidden deny-all agent markdown
|
|
29
|
+
(isolation invariant), then calls official `@opencode/client`
|
|
30
|
+
`generate.text({ location?, prompt, model: { id, providerID } })`. No coding
|
|
31
|
+
session / `promptAsync` / `tool.ids` path. Empty text is never success.
|
|
31
32
|
- `resolve.js` — model selection, mirroring OpenCode's `getSmallModel` chain:
|
|
32
33
|
0. OpenChamber's own settings override (Settings → Sessions → Small Model):
|
|
33
34
|
when `smallModelUseDefault` is `false`, `smallModelOverride`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OpenCode } from '@opencode/client';
|
|
2
2
|
|
|
3
3
|
// Directory-scoped OpenCode provider catalog for small-model resolution.
|
|
4
4
|
// Never contacts models.dev — source of truth is client.config.providers().
|
|
@@ -46,6 +46,51 @@ const normalizeDirectoryKey = (directory) => {
|
|
|
46
46
|
|
|
47
47
|
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
48
48
|
|
|
49
|
+
// Compose official v2 provider.list + model.list into the SDK catalog shape
|
|
50
|
+
// consumed by toSmallModelCatalog. Missing arrays are failure, not empty success.
|
|
51
|
+
const composeV2ProviderCatalogSource = (providers, models) => {
|
|
52
|
+
if (!Array.isArray(providers) || !Array.isArray(models)) return null;
|
|
53
|
+
|
|
54
|
+
const modelsByProvider = new Map();
|
|
55
|
+
for (const model of models) {
|
|
56
|
+
if (!isRecord(model)) continue;
|
|
57
|
+
const providerID = typeof model.providerID === 'string' ? model.providerID : '';
|
|
58
|
+
const modelID = typeof model.modelID === 'string' && model.modelID
|
|
59
|
+
? model.modelID
|
|
60
|
+
: (typeof model.id === 'string' ? model.id : '');
|
|
61
|
+
if (!providerID || !modelID) continue;
|
|
62
|
+
if (!modelsByProvider.has(providerID)) modelsByProvider.set(providerID, Object.create(null));
|
|
63
|
+
const entry = { id: modelID };
|
|
64
|
+
if (typeof model.family === 'string' && model.family) entry.family = model.family;
|
|
65
|
+
if (isRecord(model.limit)) {
|
|
66
|
+
const limit = Object.create(null);
|
|
67
|
+
if (Number.isFinite(model.limit.context)) limit.context = model.limit.context;
|
|
68
|
+
if (Number.isFinite(model.limit.output)) limit.output = model.limit.output;
|
|
69
|
+
if (Object.keys(limit).length > 0) entry.limit = limit;
|
|
70
|
+
}
|
|
71
|
+
if (isRecord(model.api) && typeof model.api.url === 'string' && model.api.url.trim()) {
|
|
72
|
+
entry.api = { url: model.api.url.trim() };
|
|
73
|
+
}
|
|
74
|
+
if (typeof model.time?.released === 'number' && Number.isFinite(model.time.released)) {
|
|
75
|
+
entry.release_date = new Date(model.time.released).toISOString().slice(0, 10);
|
|
76
|
+
} else if (typeof model.release_date === 'string' && model.release_date) {
|
|
77
|
+
entry.release_date = model.release_date;
|
|
78
|
+
}
|
|
79
|
+
modelsByProvider.get(providerID)[modelID] = entry;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const catalogProviders = [];
|
|
83
|
+
for (const provider of providers) {
|
|
84
|
+
if (!isRecord(provider) || typeof provider.id !== 'string' || !provider.id) continue;
|
|
85
|
+
catalogProviders.push({
|
|
86
|
+
id: provider.id,
|
|
87
|
+
name: typeof provider.name === 'string' && provider.name ? provider.name : provider.id,
|
|
88
|
+
models: modelsByProvider.get(provider.id) || Object.create(null),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return { providers: catalogProviders };
|
|
92
|
+
};
|
|
93
|
+
|
|
49
94
|
/**
|
|
50
95
|
* Convert the official SDK provider list into the internal small-model catalog
|
|
51
96
|
* shape. Keeps family / release_date / limit / cost.input|output /
|
|
@@ -126,17 +171,22 @@ export function createModelCatalogLoader({
|
|
|
126
171
|
|
|
127
172
|
const fetchOpenCodeCatalog = async (directory) => {
|
|
128
173
|
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
129
|
-
const client =
|
|
174
|
+
const client = OpenCode.make({
|
|
130
175
|
baseUrl,
|
|
131
|
-
...(directory ? { directory } : {}),
|
|
132
176
|
headers: getOpenCodeAuthHeaders(),
|
|
133
177
|
fetch: (request) => fetchImpl(request, { signal: AbortSignal.timeout(timeoutMs) }),
|
|
134
178
|
});
|
|
135
|
-
const
|
|
136
|
-
|
|
179
|
+
const location = directory ? { directory } : undefined;
|
|
180
|
+
const request = location ? { location } : undefined;
|
|
181
|
+
const [providersResult, modelsResult] = await Promise.all([
|
|
182
|
+
client.provider.list(request),
|
|
183
|
+
client.model.list(request),
|
|
184
|
+
]);
|
|
185
|
+
const source = composeV2ProviderCatalogSource(providersResult?.data, modelsResult?.data);
|
|
186
|
+
if (!source) {
|
|
137
187
|
throw new Error('OpenCode provider catalog is unavailable');
|
|
138
188
|
}
|
|
139
|
-
const catalog = toSmallModelCatalog(
|
|
189
|
+
const catalog = toSmallModelCatalog(source);
|
|
140
190
|
if (!catalog) {
|
|
141
191
|
throw new Error('OpenCode provider catalog returned an unexpected payload');
|
|
142
192
|
}
|
|
@@ -68,6 +68,53 @@ describe('createModelCatalogLoader', () => {
|
|
|
68
68
|
expect(Object.keys(catalogA).length).toBeGreaterThan(0);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
it('composes official v2 provider.list + model.list into the small-model catalog', async () => {
|
|
72
|
+
const loader = createModelCatalogLoader({
|
|
73
|
+
buildOpenCodeUrl: () => 'http://127.0.0.1:4096/',
|
|
74
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic test' }),
|
|
75
|
+
fetchImpl: async (url) => {
|
|
76
|
+
const href = String(url);
|
|
77
|
+
const data = href.includes('/api/model')
|
|
78
|
+
? [{
|
|
79
|
+
id: 'gemini-2.5-flash',
|
|
80
|
+
modelID: 'gemini-2.5-flash',
|
|
81
|
+
providerID: 'google',
|
|
82
|
+
family: 'gemini-flash',
|
|
83
|
+
name: 'Gemini 2.5 Flash',
|
|
84
|
+
capabilities: {},
|
|
85
|
+
variants: [],
|
|
86
|
+
time: { released: Date.parse('2025-06-01T00:00:00Z') },
|
|
87
|
+
cost: [],
|
|
88
|
+
status: 'active',
|
|
89
|
+
enabled: true,
|
|
90
|
+
limit: { context: 1_000_000, output: 8192 },
|
|
91
|
+
api: { url: 'https://generativelanguage.googleapis.com' },
|
|
92
|
+
}]
|
|
93
|
+
: [{ id: 'google', name: 'Google', package: '@ai-sdk/google' }];
|
|
94
|
+
return new Response(JSON.stringify({
|
|
95
|
+
location: { directory: '/proj', project: { id: 'p', directory: '/proj', canonical: '/proj' } },
|
|
96
|
+
data,
|
|
97
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const catalog = await loader.getModelCatalog('/proj');
|
|
102
|
+
expect(catalog).not.toBe(MINIMAL_FALLBACK_CATALOG);
|
|
103
|
+
expect(catalog.google).toEqual({
|
|
104
|
+
id: 'google',
|
|
105
|
+
name: 'Google',
|
|
106
|
+
models: {
|
|
107
|
+
'gemini-2.5-flash': {
|
|
108
|
+
id: 'gemini-2.5-flash',
|
|
109
|
+
family: 'gemini-flash',
|
|
110
|
+
release_date: '2025-06-01',
|
|
111
|
+
limit: { context: 1_000_000, output: 8192 },
|
|
112
|
+
api: { url: 'https://generativelanguage.googleapis.com' },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
71
118
|
it('normalizes directory keys so trailing slashes share a bucket', () => {
|
|
72
119
|
const loader = createModelCatalogLoader({
|
|
73
120
|
buildOpenCodeUrl: () => 'http://127.0.0.1:9/',
|
|
@@ -3,7 +3,7 @@ import os from 'os';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { readAuthFile } from '../opencode/auth.js';
|
|
5
5
|
import { readConfigLayers } from '../opencode/shared.js';
|
|
6
|
-
import {
|
|
6
|
+
import { OpenCode } from '@opencode/client';
|
|
7
7
|
import { loadConnectedCatalog } from '../llm/catalog.js';
|
|
8
8
|
import { createModelCatalogLoader, getCatalogProvider } from './catalog.js';
|
|
9
9
|
import { resolveSmallModel, parseModelRef, isUsableAuthEntry, getAuthEntryForProvider } from './resolve.js';
|
|
@@ -215,11 +215,11 @@ export function createSmallModelService(dependencies) {
|
|
|
215
215
|
getOpenCodeAuthHeaders,
|
|
216
216
|
}).getModelCatalog;
|
|
217
217
|
const getConnectedCatalog = dependencies.getConnectedCatalog || (async (directory) => {
|
|
218
|
-
const client =
|
|
218
|
+
const client = OpenCode.make({
|
|
219
219
|
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
220
|
-
...(directory ? { directory } : {}),
|
|
221
220
|
headers: getOpenCodeAuthHeaders(),
|
|
222
221
|
});
|
|
222
|
+
void directory;
|
|
223
223
|
return loadConnectedCatalog(client);
|
|
224
224
|
});
|
|
225
225
|
|
|
@@ -2,17 +2,18 @@ import fs from 'fs';
|
|
|
2
2
|
import fsPromises from 'fs/promises';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import { OpenCode } from '@opencode/client';
|
|
6
6
|
|
|
7
|
-
// Temporary OpenCode
|
|
8
|
-
//
|
|
7
|
+
// Temporary OpenCode path for providers that lack a dedicated small-model
|
|
8
|
+
// adapter (plugin providers, region/credential-chain, …).
|
|
9
9
|
// Auth, endpoint rewrite, and token refresh stay inside the OpenCode runtime.
|
|
10
|
+
//
|
|
11
|
+
// Prefer official `generate.text` (no session). The deny-all agent markdown in
|
|
12
|
+
// the lazy temp directory remains available if a future caller needs a session
|
|
13
|
+
// workspace, but pure-text small-model calls must not open coding sessions.
|
|
10
14
|
|
|
11
15
|
export const SMALL_MODEL_AGENT_NAME = 'openchamber-smallmodel';
|
|
12
16
|
export const SESSION_SETTLE_TIMEOUT_MS = 60_000;
|
|
13
|
-
const SESSION_SETTLE_POLL_MS = 1_000;
|
|
14
|
-
const ARCHIVE_RETRY_MS = 25;
|
|
15
|
-
const INCOMPLETE_ASSISTANT_SETTLE_PROBES = 2;
|
|
16
17
|
|
|
17
18
|
const AGENT_MARKDOWN = `---
|
|
18
19
|
mode: primary
|
|
@@ -31,64 +32,9 @@ let tempDirectory = null;
|
|
|
31
32
|
/** @type {Promise<string> | null} */
|
|
32
33
|
let tempDirectoryInflight = null;
|
|
33
34
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|| result?.error?.code === 'not_found'
|
|
38
|
-
|| result?.status === 404;
|
|
39
|
-
|
|
40
|
-
const promptAdmitted = (result) =>
|
|
41
|
-
!result?.error
|
|
42
|
-
&& (result?.response?.status === 204
|
|
43
|
-
|| result?.status === 204
|
|
44
|
-
|| result?.data !== undefined
|
|
45
|
-
|| result?.response?.ok === true);
|
|
46
|
-
|
|
47
|
-
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
48
|
-
if (signal?.aborted) {
|
|
49
|
-
reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const timer = setTimeout(resolve, ms);
|
|
53
|
-
const onAbort = () => {
|
|
54
|
-
clearTimeout(timer);
|
|
55
|
-
reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));
|
|
56
|
-
};
|
|
57
|
-
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const sdkErrorMessage = (result, fallback) => {
|
|
61
|
-
const status = result?.error?.status ?? result?.error?.statusCode ?? result?.status;
|
|
62
|
-
const message = result?.error?.message || result?.error?.data?.message || fallback;
|
|
63
|
-
return status ? `${message} (${status})` : message;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const readMessageInfo = (message) => {
|
|
67
|
-
if (!message || typeof message !== 'object') return null;
|
|
68
|
-
if (message.info && typeof message.info === 'object') return message.info;
|
|
69
|
-
return message;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const assistantTextFromMessages = (messages) => {
|
|
73
|
-
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
74
|
-
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
75
|
-
const message = messages[i];
|
|
76
|
-
const info = readMessageInfo(message);
|
|
77
|
-
if (info?.role !== 'assistant') continue;
|
|
78
|
-
if (info.error) {
|
|
79
|
-
const detail = typeof info.error === 'string'
|
|
80
|
-
? info.error
|
|
81
|
-
: (info.error?.message || JSON.stringify(info.error));
|
|
82
|
-
throw new Error(`OpenCode small-model session assistant error: ${detail}`);
|
|
83
|
-
}
|
|
84
|
-
const parts = Array.isArray(message?.parts) ? message.parts : [];
|
|
85
|
-
const text = parts
|
|
86
|
-
.map((part) => (part?.type === 'text' && typeof part.text === 'string' ? part.text : ''))
|
|
87
|
-
.filter(Boolean)
|
|
88
|
-
.join('');
|
|
89
|
-
return text;
|
|
90
|
-
}
|
|
91
|
-
return '';
|
|
35
|
+
const clientErrorMessage = (error, fallback) => {
|
|
36
|
+
if (typeof error?.message === 'string' && error.message.trim()) return error.message;
|
|
37
|
+
return fallback;
|
|
92
38
|
};
|
|
93
39
|
|
|
94
40
|
const ensureTempDirectory = async () => {
|
|
@@ -112,100 +58,16 @@ const ensureTempDirectory = async () => {
|
|
|
112
58
|
return tempDirectoryInflight;
|
|
113
59
|
};
|
|
114
60
|
|
|
115
|
-
const createClient = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders
|
|
61
|
+
const createClient = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders }) => {
|
|
116
62
|
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
117
|
-
return
|
|
63
|
+
return OpenCode.make({
|
|
118
64
|
baseUrl,
|
|
119
|
-
directory,
|
|
120
65
|
headers: getOpenCodeAuthHeaders(),
|
|
121
66
|
});
|
|
122
67
|
};
|
|
123
68
|
|
|
124
|
-
const archiveCreatedSession = async (client, sessionID, directory) => {
|
|
125
|
-
const archiveAt = Date.now();
|
|
126
|
-
const update = () => client.session.update({
|
|
127
|
-
sessionID,
|
|
128
|
-
directory,
|
|
129
|
-
time: { archived: archiveAt },
|
|
130
|
-
});
|
|
131
|
-
let result = await update();
|
|
132
|
-
if (isMissing(result)) {
|
|
133
|
-
await sleep(ARCHIVE_RETRY_MS);
|
|
134
|
-
result = await update();
|
|
135
|
-
}
|
|
136
|
-
if (result?.error || isMissing(result)) {
|
|
137
|
-
throw new Error(`OpenCode small-model session archive failed: ${sdkErrorMessage(result, 'archive failed')}`);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const waitForIdleAssistant = async ({ client, sessionID, directory, signal }) => {
|
|
142
|
-
let incompleteAssistantProbes = 0;
|
|
143
|
-
let emptyIdleProbes = 0;
|
|
144
|
-
|
|
145
|
-
for (;;) {
|
|
146
|
-
signal?.throwIfAborted?.();
|
|
147
|
-
|
|
148
|
-
let sessionBusy = false;
|
|
149
|
-
try {
|
|
150
|
-
const statusResult = await client.session.status({ directory }, { signal });
|
|
151
|
-
if (!statusResult?.error && statusResult?.data && typeof statusResult.data === 'object') {
|
|
152
|
-
const statusValue = statusResult.data[sessionID];
|
|
153
|
-
const type = statusValue?.type ?? statusValue?.status;
|
|
154
|
-
sessionBusy = type === 'busy' || type === 'retry';
|
|
155
|
-
}
|
|
156
|
-
} catch (error) {
|
|
157
|
-
if (signal?.aborted) throw error;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (sessionBusy) {
|
|
161
|
-
incompleteAssistantProbes = 0;
|
|
162
|
-
emptyIdleProbes = 0;
|
|
163
|
-
await sleep(SESSION_SETTLE_POLL_MS, signal);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const messagesResult = await client.session.messages({
|
|
169
|
-
sessionID,
|
|
170
|
-
directory,
|
|
171
|
-
limit: 20,
|
|
172
|
-
}, { signal });
|
|
173
|
-
if (!messagesResult?.error && Array.isArray(messagesResult?.data)) {
|
|
174
|
-
const lastInfo = readMessageInfo(messagesResult.data.at(-1));
|
|
175
|
-
if (lastInfo?.role === 'assistant') {
|
|
176
|
-
emptyIdleProbes = 0;
|
|
177
|
-
if (lastInfo.error) {
|
|
178
|
-
const detail = typeof lastInfo.error === 'string'
|
|
179
|
-
? lastInfo.error
|
|
180
|
-
: (lastInfo.error?.message || JSON.stringify(lastInfo.error));
|
|
181
|
-
throw new Error(`OpenCode small-model session assistant error: ${detail}`);
|
|
182
|
-
}
|
|
183
|
-
if (lastInfo.time?.completed) {
|
|
184
|
-
return messagesResult.data;
|
|
185
|
-
}
|
|
186
|
-
incompleteAssistantProbes += 1;
|
|
187
|
-
if (incompleteAssistantProbes >= INCOMPLETE_ASSISTANT_SETTLE_PROBES) {
|
|
188
|
-
return messagesResult.data;
|
|
189
|
-
}
|
|
190
|
-
} else {
|
|
191
|
-
incompleteAssistantProbes = 0;
|
|
192
|
-
emptyIdleProbes += 1;
|
|
193
|
-
if (emptyIdleProbes >= 5 && lastInfo?.role === 'user') {
|
|
194
|
-
throw new Error('OpenCode small-model session ended without assistant response');
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
} catch (error) {
|
|
199
|
-
if (signal?.aborted) throw error;
|
|
200
|
-
if (error instanceof Error && error.message.startsWith('OpenCode small-model')) throw error;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
await sleep(SESSION_SETTLE_POLL_MS, signal);
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
|
|
207
69
|
/**
|
|
208
|
-
* Generate text via
|
|
70
|
+
* Generate text via official OpenCode `generate.text`.
|
|
209
71
|
* Errors are never masked as empty success.
|
|
210
72
|
*
|
|
211
73
|
* @param {{
|
|
@@ -217,6 +79,7 @@ const waitForIdleAssistant = async ({ client, sessionID, directory, signal }) =>
|
|
|
217
79
|
* system?: string,
|
|
218
80
|
* purpose?: string,
|
|
219
81
|
* directory?: string,
|
|
82
|
+
* settleTimeoutMs?: number,
|
|
220
83
|
* }} options
|
|
221
84
|
*/
|
|
222
85
|
export async function generateViaOpenCodeSession({
|
|
@@ -226,7 +89,8 @@ export async function generateViaOpenCodeSession({
|
|
|
226
89
|
modelID,
|
|
227
90
|
prompt,
|
|
228
91
|
system,
|
|
229
|
-
purpose,
|
|
92
|
+
purpose: _purpose,
|
|
93
|
+
directory,
|
|
230
94
|
settleTimeoutMs = SESSION_SETTLE_TIMEOUT_MS,
|
|
231
95
|
}) {
|
|
232
96
|
if (typeof prompt !== 'string' || !prompt.trim()) {
|
|
@@ -236,79 +100,59 @@ export async function generateViaOpenCodeSession({
|
|
|
236
100
|
throw new Error('OpenCode small-model session requires providerID and modelID');
|
|
237
101
|
}
|
|
238
102
|
|
|
239
|
-
const purposeLabel = typeof purpose === 'string' && purpose.trim() ? purpose.trim() : 'utility';
|
|
240
103
|
const budgetMs = Number(settleTimeoutMs) > 0 ? Number(settleTimeoutMs) : SESSION_SETTLE_TIMEOUT_MS;
|
|
241
|
-
|
|
104
|
+
// Keep the deny-all agent workspace warm for isolation invariants / future
|
|
105
|
+
// session needs, but pure text uses generate.text (no coding session).
|
|
106
|
+
await ensureTempDirectory();
|
|
107
|
+
|
|
242
108
|
const client = createClient({
|
|
243
109
|
buildOpenCodeUrl,
|
|
244
110
|
getOpenCodeAuthHeaders,
|
|
245
|
-
directory: workingDirectory,
|
|
246
111
|
});
|
|
247
112
|
|
|
248
|
-
|
|
113
|
+
const locationDirectory = typeof directory === 'string' && directory.trim()
|
|
114
|
+
? directory.trim()
|
|
115
|
+
: null;
|
|
116
|
+
const location = locationDirectory ? { directory: locationDirectory } : undefined;
|
|
117
|
+
|
|
118
|
+
const fullPrompt = [
|
|
119
|
+
typeof system === 'string' && system.trim() ? system.trim() : '',
|
|
120
|
+
prompt.trim(),
|
|
121
|
+
].filter(Boolean).join('\n\n');
|
|
122
|
+
|
|
249
123
|
const controller = new AbortController();
|
|
250
124
|
const timeout = setTimeout(() => {
|
|
251
|
-
controller.abort(new Error(`OpenCode small-model
|
|
125
|
+
controller.abort(new Error(`OpenCode small-model generate timed out after ${budgetMs}ms`));
|
|
252
126
|
}, budgetMs);
|
|
253
127
|
|
|
254
128
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
throw new Error(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
// Archive before prompting so ordinary session lists never flash system sessions.
|
|
272
|
-
await archiveCreatedSession(client, sessionID, workingDirectory);
|
|
273
|
-
|
|
274
|
-
const promptResult = await client.session.promptAsync({
|
|
275
|
-
sessionID,
|
|
276
|
-
directory: workingDirectory,
|
|
277
|
-
agent: SMALL_MODEL_AGENT_NAME,
|
|
278
|
-
model: { providerID, modelID },
|
|
279
|
-
...(typeof system === 'string' && system.trim() ? { system: system.trim() } : {}),
|
|
280
|
-
parts: [{ type: 'text', text: prompt, synthetic: false }],
|
|
281
|
-
}, { signal: controller.signal });
|
|
282
|
-
|
|
283
|
-
if (!promptAdmitted(promptResult)) {
|
|
284
|
-
throw new Error(`OpenCode small-model prompt_async failed: ${sdkErrorMessage(promptResult, 'prompt_async failed')}`);
|
|
129
|
+
let result;
|
|
130
|
+
try {
|
|
131
|
+
result = await client.generate.text({
|
|
132
|
+
...(location ? { location } : {}),
|
|
133
|
+
prompt: fullPrompt,
|
|
134
|
+
model: { id: modelID, providerID },
|
|
135
|
+
}, { signal: controller.signal });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (controller.signal.aborted) {
|
|
138
|
+
throw controller.signal.reason instanceof Error
|
|
139
|
+
? controller.signal.reason
|
|
140
|
+
: new Error(`OpenCode small-model generate timed out after ${budgetMs}ms`);
|
|
141
|
+
}
|
|
142
|
+
throw new Error(
|
|
143
|
+
`OpenCode small-model generate.text failed: ${clientErrorMessage(error, 'generate.text failed')}`,
|
|
144
|
+
);
|
|
285
145
|
}
|
|
286
146
|
|
|
287
|
-
const
|
|
288
|
-
client,
|
|
289
|
-
sessionID,
|
|
290
|
-
directory: workingDirectory,
|
|
291
|
-
signal: controller.signal,
|
|
292
|
-
});
|
|
293
|
-
const text = assistantTextFromMessages(messages);
|
|
147
|
+
const text = typeof result?.text === 'string' ? result.text : '';
|
|
294
148
|
if (!text.trim()) {
|
|
295
|
-
throw new Error('OpenCode small-model
|
|
149
|
+
throw new Error('OpenCode small-model generate.text returned no assistant text');
|
|
296
150
|
}
|
|
297
151
|
return text;
|
|
298
152
|
} finally {
|
|
299
153
|
clearTimeout(timeout);
|
|
300
|
-
if (sessionID) {
|
|
301
|
-
try {
|
|
302
|
-
await client.session.delete({ sessionID, directory: workingDirectory });
|
|
303
|
-
} catch (error) {
|
|
304
|
-
console.warn(
|
|
305
|
-
'[small-model] failed to delete temporary OpenCode session:',
|
|
306
|
-
error?.message || error,
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
154
|
}
|
|
311
|
-
}
|
|
155
|
+
}
|
|
312
156
|
|
|
313
157
|
/** Best-effort cleanup of the lazy temp directory (agent markdown + empty root). */
|
|
314
158
|
export async function stop() {
|