@glwhappen/web-code 1.32.9 → 1.32.10
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.de.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.tr.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/api-docs.html +6 -7
- package/dist/assets/{index-D_7CSvqO.js → index-BLLsK3sG.js} +276 -261
- package/dist/assets/index-Dl5QP21C.css +32 -0
- package/dist/index.html +2 -2
- package/dist/modelConstants.js +841 -0
- package/dist-server/server/claude-sdk.js +57 -34
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +6 -3
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +3 -1
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/gemini-response-handler.js +34 -0
- package/dist-server/server/gemini-response-handler.js.map +1 -1
- package/dist-server/server/index.js +131 -19
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/database/index.js +1 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-management.service.js +1 -0
- package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +4 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +143 -0
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -39
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +2 -15
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +27 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +3 -9
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
- package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
- package/dist-server/server/modules/providers/provider.registry.js +2 -0
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +42 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
- package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +199 -0
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +7 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- package/dist-server/server/modules/providers/tests/mcp.test.js +73 -6
- package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +264 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +270 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/skills.test.js +33 -0
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +18 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/openai-codex.js +32 -4
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +287 -0
- package/dist-server/server/opencode-cli.js.map +1 -0
- package/dist-server/server/opencode-cli.test.js +84 -0
- package/dist-server/server/opencode-cli.test.js.map +1 -0
- package/dist-server/server/routes/agent.js +21 -8
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/commands.js +202 -209
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/cursor.js +2 -2
- package/dist-server/server/routes/cursor.js.map +1 -1
- package/dist-server/server/routes/settings.js +0 -10
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/routes/tests/commands.test.js +76 -0
- package/dist-server/server/routes/tests/commands.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +286 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/package.json +3 -1
- package/public/api-docs.html +878 -0
- package/public/modelConstants.js +841 -0
- package/server/claude-sdk.js +64 -35
- package/server/cursor-cli.js +6 -3
- package/server/gemini-cli.js +7 -1
- package/server/gemini-response-handler.js +38 -0
- package/server/index.js +150 -19
- package/server/modules/database/index.ts +1 -0
- package/server/modules/projects/services/project-management.service.ts +2 -0
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +7 -1
- package/server/modules/providers/README.md +11 -3
- package/server/modules/providers/list/claude/claude-models.provider.ts +193 -0
- package/server/modules/providers/list/claude/claude.provider.ts +3 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
- package/server/modules/providers/list/codex/codex-skills.provider.ts +10 -50
- package/server/modules/providers/list/codex/codex.provider.ts +3 -0
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +7 -20
- package/server/modules/providers/list/cursor/cursor.provider.ts +3 -0
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +3 -10
- package/server/modules/providers/list/gemini/gemini.provider.ts +3 -0
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
- package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +158 -0
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
- package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
- package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
- package/server/modules/providers/provider.registry.ts +2 -0
- package/server/modules/providers/provider.routes.ts +62 -2
- package/server/modules/providers/services/mcp.service.ts +1 -12
- package/server/modules/providers/services/provider-models.service.ts +325 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +8 -0
- package/server/modules/providers/shared/base/abstract.provider.ts +2 -0
- package/server/modules/providers/tests/mcp.test.ts +93 -6
- package/server/modules/providers/tests/opencode-models.test.ts +73 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +336 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
- package/server/modules/providers/tests/skills.test.ts +66 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +21 -1
- package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
- package/server/openai-codex.js +40 -4
- package/server/opencode-cli.js +336 -0
- package/server/opencode-cli.test.js +95 -0
- package/server/routes/agent.js +22 -8
- package/server/routes/commands.js +254 -233
- package/server/routes/cursor.js +2 -2
- package/server/routes/settings.js +1 -10
- package/server/routes/tests/commands.test.js +82 -0
- package/server/shared/interfaces.ts +45 -0
- package/server/shared/types.ts +88 -1
- package/server/shared/utils.ts +384 -0
- package/dist/assets/index-DdxLnCfK.css +0 -32
- package/dist-server/shared/modelConstants.js +0 -99
- package/dist-server/shared/modelConstants.js.map +0 -1
- package/shared/modelConstants.js +0 -107
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
|
|
9
|
+
import { closeConnection, initializeDatabase, sessionsDb, userDb } from '@/modules/database/index.js';
|
|
10
|
+
import { OpenCodeSessionSynchronizer } from '@/modules/providers/list/opencode/opencode-session-synchronizer.provider.js';
|
|
11
|
+
import { OpenCodeSessionsProvider } from '@/modules/providers/list/opencode/opencode-sessions.provider.js';
|
|
12
|
+
|
|
13
|
+
const patchHomeDir = (nextHomeDir: string) => {
|
|
14
|
+
const original = os.homedir;
|
|
15
|
+
(os as any).homedir = () => nextHomeDir;
|
|
16
|
+
return () => {
|
|
17
|
+
(os as any).homedir = original;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
22
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
23
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'opencode-provider-db-'));
|
|
24
|
+
const databasePath = path.join(tempDirectory, 'auth.db');
|
|
25
|
+
|
|
26
|
+
closeConnection();
|
|
27
|
+
process.env.DATABASE_PATH = databasePath;
|
|
28
|
+
await initializeDatabase();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await runTest();
|
|
32
|
+
} finally {
|
|
33
|
+
closeConnection();
|
|
34
|
+
if (previousDatabasePath === undefined) {
|
|
35
|
+
delete process.env.DATABASE_PATH;
|
|
36
|
+
} else {
|
|
37
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
38
|
+
}
|
|
39
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const createOpenCodeDatabase = async (homeDir: string, workspacePath: string): Promise<void> => {
|
|
44
|
+
const dataDir = path.join(homeDir, '.local', 'share', 'opencode');
|
|
45
|
+
await mkdir(dataDir, { recursive: true });
|
|
46
|
+
|
|
47
|
+
const db = new Database(path.join(dataDir, 'opencode.db'));
|
|
48
|
+
try {
|
|
49
|
+
db.exec(`
|
|
50
|
+
CREATE TABLE project (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
worktree TEXT NOT NULL,
|
|
53
|
+
vcs TEXT,
|
|
54
|
+
name TEXT,
|
|
55
|
+
icon_url TEXT,
|
|
56
|
+
icon_color TEXT,
|
|
57
|
+
time_created INTEGER NOT NULL,
|
|
58
|
+
time_updated INTEGER NOT NULL,
|
|
59
|
+
time_initialized INTEGER,
|
|
60
|
+
sandboxes TEXT NOT NULL,
|
|
61
|
+
commands TEXT,
|
|
62
|
+
icon_url_override TEXT
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE TABLE session (
|
|
66
|
+
id TEXT PRIMARY KEY,
|
|
67
|
+
project_id TEXT NOT NULL,
|
|
68
|
+
parent_id TEXT,
|
|
69
|
+
slug TEXT NOT NULL,
|
|
70
|
+
directory TEXT NOT NULL,
|
|
71
|
+
title TEXT NOT NULL,
|
|
72
|
+
version TEXT NOT NULL,
|
|
73
|
+
share_url TEXT,
|
|
74
|
+
summary_additions INTEGER,
|
|
75
|
+
summary_deletions INTEGER,
|
|
76
|
+
summary_files INTEGER,
|
|
77
|
+
summary_diffs TEXT,
|
|
78
|
+
revert TEXT,
|
|
79
|
+
permission TEXT,
|
|
80
|
+
time_created INTEGER NOT NULL,
|
|
81
|
+
time_updated INTEGER NOT NULL,
|
|
82
|
+
time_compacting INTEGER,
|
|
83
|
+
time_archived INTEGER,
|
|
84
|
+
workspace_id TEXT,
|
|
85
|
+
path TEXT,
|
|
86
|
+
agent TEXT,
|
|
87
|
+
model TEXT,
|
|
88
|
+
cost REAL NOT NULL DEFAULT 0,
|
|
89
|
+
tokens_input INTEGER NOT NULL DEFAULT 0,
|
|
90
|
+
tokens_output INTEGER NOT NULL DEFAULT 0,
|
|
91
|
+
tokens_reasoning INTEGER NOT NULL DEFAULT 0,
|
|
92
|
+
tokens_cache_read INTEGER NOT NULL DEFAULT 0,
|
|
93
|
+
tokens_cache_write INTEGER NOT NULL DEFAULT 0,
|
|
94
|
+
FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
CREATE TABLE message (
|
|
98
|
+
id TEXT PRIMARY KEY,
|
|
99
|
+
session_id TEXT NOT NULL,
|
|
100
|
+
time_created INTEGER NOT NULL,
|
|
101
|
+
time_updated INTEGER NOT NULL,
|
|
102
|
+
data TEXT NOT NULL,
|
|
103
|
+
FOREIGN KEY (session_id) REFERENCES session(id) ON DELETE CASCADE
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
CREATE TABLE part (
|
|
107
|
+
id TEXT PRIMARY KEY,
|
|
108
|
+
message_id TEXT NOT NULL,
|
|
109
|
+
session_id TEXT NOT NULL,
|
|
110
|
+
time_created INTEGER NOT NULL,
|
|
111
|
+
time_updated INTEGER NOT NULL,
|
|
112
|
+
data TEXT NOT NULL,
|
|
113
|
+
FOREIGN KEY (message_id) REFERENCES message(id) ON DELETE CASCADE
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
CREATE INDEX part_session_idx ON part (session_id);
|
|
117
|
+
CREATE INDEX session_project_idx ON session (project_id);
|
|
118
|
+
CREATE INDEX message_session_time_created_id_idx ON message (session_id, time_created, id);
|
|
119
|
+
CREATE INDEX part_message_id_id_idx ON part (message_id, id);
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
db.prepare(
|
|
123
|
+
'INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES (?, ?, ?, ?, ?)',
|
|
124
|
+
).run(
|
|
125
|
+
'project-1',
|
|
126
|
+
workspacePath,
|
|
127
|
+
1_700_000_000_000,
|
|
128
|
+
1_700_000_001_000,
|
|
129
|
+
'[]',
|
|
130
|
+
);
|
|
131
|
+
db.prepare(`
|
|
132
|
+
INSERT INTO session (
|
|
133
|
+
id, project_id, slug, directory, title, version, time_created, time_updated, time_archived,
|
|
134
|
+
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write
|
|
135
|
+
)
|
|
136
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
137
|
+
`).run(
|
|
138
|
+
'open-session-1',
|
|
139
|
+
'project-1',
|
|
140
|
+
'open-session-1',
|
|
141
|
+
workspacePath,
|
|
142
|
+
'OpenCode indexed title',
|
|
143
|
+
'0.0.0',
|
|
144
|
+
1_700_000_000_000,
|
|
145
|
+
1_700_000_004_000,
|
|
146
|
+
null,
|
|
147
|
+
10,
|
|
148
|
+
20,
|
|
149
|
+
7,
|
|
150
|
+
3,
|
|
151
|
+
2,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const userMessageData = JSON.stringify({
|
|
155
|
+
role: 'user',
|
|
156
|
+
time: { created: 1_700_000_001_000 },
|
|
157
|
+
agent: 'test',
|
|
158
|
+
model: { providerID: 'anthropic', modelID: 'claude' },
|
|
159
|
+
});
|
|
160
|
+
const assistantMessageData = JSON.stringify({
|
|
161
|
+
role: 'assistant',
|
|
162
|
+
time: { created: 1_700_000_002_000, completed: 1_700_000_003_000 },
|
|
163
|
+
parentID: 'message-user',
|
|
164
|
+
modelID: 'anthropic/claude-sonnet-4-5',
|
|
165
|
+
providerID: 'anthropic',
|
|
166
|
+
mode: 'default',
|
|
167
|
+
agent: 'test',
|
|
168
|
+
path: { cwd: '.', root: '.' },
|
|
169
|
+
cost: 0.01,
|
|
170
|
+
tokens: {
|
|
171
|
+
input: 10,
|
|
172
|
+
output: 20,
|
|
173
|
+
reasoning: 0,
|
|
174
|
+
cache: { read: 3, write: 2 },
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
db.prepare(
|
|
179
|
+
'INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)',
|
|
180
|
+
).run('message-user', 'open-session-1', 1_700_000_001_000, 1_700_000_001_500, userMessageData);
|
|
181
|
+
db.prepare(
|
|
182
|
+
'INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)',
|
|
183
|
+
).run('message-assistant', 'open-session-1', 1_700_000_002_000, 1_700_000_003_000, assistantMessageData);
|
|
184
|
+
|
|
185
|
+
const insertPart = db.prepare(`
|
|
186
|
+
INSERT INTO part (id, message_id, session_id, time_created, time_updated, data)
|
|
187
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
188
|
+
`);
|
|
189
|
+
insertPart.run(
|
|
190
|
+
'part-user-text',
|
|
191
|
+
'message-user',
|
|
192
|
+
'open-session-1',
|
|
193
|
+
1_700_000_001_000,
|
|
194
|
+
1_700_000_001_000,
|
|
195
|
+
JSON.stringify({
|
|
196
|
+
type: 'text',
|
|
197
|
+
text: JSON.stringify('Build the OpenCode integration.'),
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
insertPart.run(
|
|
201
|
+
'part-reasoning',
|
|
202
|
+
'message-assistant',
|
|
203
|
+
'open-session-1',
|
|
204
|
+
1_700_000_002_000,
|
|
205
|
+
1_700_000_002_000,
|
|
206
|
+
JSON.stringify({
|
|
207
|
+
type: 'reasoning',
|
|
208
|
+
text: 'I will inspect the provider shape first.',
|
|
209
|
+
time: { start: 0, end: 1 },
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
insertPart.run(
|
|
213
|
+
'part-assistant-text',
|
|
214
|
+
'message-assistant',
|
|
215
|
+
'open-session-1',
|
|
216
|
+
1_700_000_002_500,
|
|
217
|
+
1_700_000_002_500,
|
|
218
|
+
JSON.stringify({
|
|
219
|
+
type: 'text',
|
|
220
|
+
text: 'The provider is wired.',
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
insertPart.run(
|
|
224
|
+
'part-tool',
|
|
225
|
+
'message-assistant',
|
|
226
|
+
'open-session-1',
|
|
227
|
+
1_700_000_003_000,
|
|
228
|
+
1_700_000_003_000,
|
|
229
|
+
JSON.stringify({
|
|
230
|
+
type: 'tool',
|
|
231
|
+
tool: 'bash',
|
|
232
|
+
callID: 'tool-call-1',
|
|
233
|
+
state: {
|
|
234
|
+
status: 'completed',
|
|
235
|
+
input: { command: 'npm test' },
|
|
236
|
+
output: 'ok',
|
|
237
|
+
title: 'bash',
|
|
238
|
+
metadata: {},
|
|
239
|
+
time: { start: 0, end: 1 },
|
|
240
|
+
},
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
} finally {
|
|
244
|
+
db.close();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
test('OpenCode session synchronizer indexes sqlite sessions without deletable transcript paths', { concurrency: false }, async () => {
|
|
249
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-'));
|
|
250
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
251
|
+
await mkdir(workspacePath, { recursive: true });
|
|
252
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
await createOpenCodeDatabase(tempRoot, workspacePath);
|
|
256
|
+
await withIsolatedDatabase(() => {
|
|
257
|
+
const createdUser = userDb.createUser('test-user', 'hash');
|
|
258
|
+
const ownerUserId = Number(createdUser.id);
|
|
259
|
+
const synchronizer = new OpenCodeSessionSynchronizer();
|
|
260
|
+
const processed = synchronizer.synchronize(undefined, ownerUserId);
|
|
261
|
+
|
|
262
|
+
return Promise.resolve(processed).then((count) => {
|
|
263
|
+
assert.equal(count, 1);
|
|
264
|
+
const indexed = sessionsDb.getSessionById(ownerUserId, 'open-session-1');
|
|
265
|
+
assert.equal(indexed?.provider, 'opencode');
|
|
266
|
+
assert.equal(indexed?.project_path, workspacePath);
|
|
267
|
+
assert.equal(indexed?.custom_name, 'OpenCode indexed title');
|
|
268
|
+
assert.equal(indexed?.jsonl_path, null);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
} finally {
|
|
272
|
+
restoreHomeDir();
|
|
273
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('OpenCode sessions provider normalizes quoted live text and skips user echoes', () => {
|
|
278
|
+
const provider = new OpenCodeSessionsProvider();
|
|
279
|
+
const normalized = provider.normalizeMessage({
|
|
280
|
+
type: 'text',
|
|
281
|
+
sessionID: 'open-session-live',
|
|
282
|
+
text: JSON.stringify('hello bro'),
|
|
283
|
+
}, null);
|
|
284
|
+
|
|
285
|
+
assert.equal(normalized.length, 1);
|
|
286
|
+
assert.equal(normalized[0]?.kind, 'stream_delta');
|
|
287
|
+
assert.equal(normalized[0]?.content, 'hello bro');
|
|
288
|
+
|
|
289
|
+
const userEcho = provider.normalizeMessage({
|
|
290
|
+
type: 'text',
|
|
291
|
+
sessionID: 'open-session-live',
|
|
292
|
+
role: 'user',
|
|
293
|
+
text: 'hello bro',
|
|
294
|
+
}, null);
|
|
295
|
+
|
|
296
|
+
assert.deepEqual(userEcho, []);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('OpenCode sessions provider reads sqlite history and token usage', { concurrency: false }, async () => {
|
|
300
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-history-'));
|
|
301
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
302
|
+
await mkdir(workspacePath, { recursive: true });
|
|
303
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
await createOpenCodeDatabase(tempRoot, workspacePath);
|
|
307
|
+
const provider = new OpenCodeSessionsProvider();
|
|
308
|
+
const history = await provider.fetchHistory('open-session-1');
|
|
309
|
+
|
|
310
|
+
assert.equal(history.total, 4);
|
|
311
|
+
assert.equal(history.messages[0]?.kind, 'text');
|
|
312
|
+
assert.equal(history.messages[0]?.role, 'user');
|
|
313
|
+
assert.equal(history.messages[0]?.content, 'Build the OpenCode integration.');
|
|
314
|
+
assert.equal(history.messages[1]?.kind, 'thinking');
|
|
315
|
+
assert.equal(history.messages[2]?.content, 'The provider is wired.');
|
|
316
|
+
assert.equal(history.messages[3]?.kind, 'tool_use');
|
|
317
|
+
assert.deepEqual(history.messages[3]?.toolResult, { content: 'ok', isError: false });
|
|
318
|
+
assert.deepEqual(history.tokenUsage, {
|
|
319
|
+
used: 42,
|
|
320
|
+
inputTokens: 13,
|
|
321
|
+
outputTokens: 20,
|
|
322
|
+
breakdown: {
|
|
323
|
+
input: 13,
|
|
324
|
+
output: 20,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const paged = await provider.fetchHistory('open-session-1', { limit: 2, offset: 0 });
|
|
329
|
+
assert.equal(paged.messages.length, 2);
|
|
330
|
+
assert.equal(paged.hasMore, true);
|
|
331
|
+
assert.equal(paged.messages[0]?.content, 'The provider is wired.');
|
|
332
|
+
} finally {
|
|
333
|
+
restoreHomeDir();
|
|
334
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createProviderModelsService,
|
|
9
|
+
PROVIDER_MODELS_CACHE_TTL_MS,
|
|
10
|
+
} from '@/modules/providers/services/provider-models.service.js';
|
|
11
|
+
import type {
|
|
12
|
+
ProviderChangeActiveModelInput,
|
|
13
|
+
LLMProvider,
|
|
14
|
+
ProviderCurrentActiveModel,
|
|
15
|
+
ProviderModelsDefinition,
|
|
16
|
+
ProviderSessionActiveModelChange,
|
|
17
|
+
} from '@/shared/types.js';
|
|
18
|
+
import { writeProviderSessionActiveModelChange } from '@/shared/utils.js';
|
|
19
|
+
|
|
20
|
+
const createModels = (value: string): ProviderModelsDefinition => ({
|
|
21
|
+
OPTIONS: [{ value, label: value }],
|
|
22
|
+
DEFAULT: value,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const createCurrentActiveModel = (model: string): ProviderCurrentActiveModel => ({
|
|
26
|
+
model,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const createSessionActiveModelChange = (
|
|
30
|
+
provider: LLMProvider,
|
|
31
|
+
input: ProviderChangeActiveModelInput,
|
|
32
|
+
): ProviderSessionActiveModelChange => ({
|
|
33
|
+
provider,
|
|
34
|
+
sessionId: input.sessionId,
|
|
35
|
+
supported: true,
|
|
36
|
+
changed: true,
|
|
37
|
+
model: input.model,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const createEphemeralCachePath = (): string => path.join(
|
|
41
|
+
os.tmpdir(),
|
|
42
|
+
`provider-model-cache-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
test('provider models service delegates to the resolved provider model adapter', async () => {
|
|
46
|
+
const calls: LLMProvider[] = [];
|
|
47
|
+
const service = createProviderModelsService({
|
|
48
|
+
cachePath: createEphemeralCachePath(),
|
|
49
|
+
resolveProvider: (provider) => {
|
|
50
|
+
calls.push(provider);
|
|
51
|
+
return {
|
|
52
|
+
models: {
|
|
53
|
+
getSupportedModels: async () => createModels(`${provider}-models`),
|
|
54
|
+
getCurrentActiveModel: async () => createCurrentActiveModel(`${provider}-active`),
|
|
55
|
+
changeActiveModel: async (input) => createSessionActiveModelChange(provider, input),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const models = await service.getProviderModels('codex', { bypassCache: true });
|
|
62
|
+
|
|
63
|
+
assert.deepEqual(calls, ['codex']);
|
|
64
|
+
assert.equal(models.models.DEFAULT, 'codex-models');
|
|
65
|
+
assert.equal(models.cache.source, 'fresh');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('provider models service returns each provider adapter result without rewriting it', async () => {
|
|
69
|
+
const expectedModels: ProviderModelsDefinition = {
|
|
70
|
+
OPTIONS: [
|
|
71
|
+
{ value: 'cursor-a', label: 'Cursor A' },
|
|
72
|
+
{ value: 'cursor-b', label: 'Cursor B' },
|
|
73
|
+
],
|
|
74
|
+
DEFAULT: 'cursor-b',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const service = createProviderModelsService({
|
|
78
|
+
cachePath: createEphemeralCachePath(),
|
|
79
|
+
resolveProvider: () => ({
|
|
80
|
+
models: {
|
|
81
|
+
getSupportedModels: async () => expectedModels,
|
|
82
|
+
getCurrentActiveModel: async () => createCurrentActiveModel('cursor-active'),
|
|
83
|
+
changeActiveModel: async (input) => createSessionActiveModelChange('cursor', input),
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const models = await service.getProviderModels('cursor', { bypassCache: true });
|
|
89
|
+
|
|
90
|
+
assert.deepEqual(models.models, expectedModels);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('provider models are cached for the three-day ttl', async () => {
|
|
94
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-ttl-'));
|
|
95
|
+
let currentTime = 1_000;
|
|
96
|
+
let loadCount = 0;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const service = createProviderModelsService({
|
|
100
|
+
cachePath: path.join(tempRoot, 'models-cache.json'),
|
|
101
|
+
now: () => currentTime,
|
|
102
|
+
resolveProvider: (provider) => ({
|
|
103
|
+
models: {
|
|
104
|
+
getSupportedModels: async () => {
|
|
105
|
+
loadCount += 1;
|
|
106
|
+
return createModels(`${provider}-${loadCount}`);
|
|
107
|
+
},
|
|
108
|
+
getCurrentActiveModel: async () => createCurrentActiveModel(`${provider}-active`),
|
|
109
|
+
changeActiveModel: async (input) => createSessionActiveModelChange(provider, input),
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const first = await service.getProviderModels('codex');
|
|
115
|
+
const cached = await service.getProviderModels('codex');
|
|
116
|
+
assert.equal(loadCount, 1);
|
|
117
|
+
assert.equal(cached.models.DEFAULT, first.models.DEFAULT);
|
|
118
|
+
assert.equal(cached.cache.source, 'memory');
|
|
119
|
+
|
|
120
|
+
currentTime += PROVIDER_MODELS_CACHE_TTL_MS - 1;
|
|
121
|
+
await service.getProviderModels('codex');
|
|
122
|
+
assert.equal(loadCount, 1);
|
|
123
|
+
|
|
124
|
+
currentTime += 2;
|
|
125
|
+
const refreshed = await service.getProviderModels('codex');
|
|
126
|
+
assert.equal(loadCount, 2);
|
|
127
|
+
assert.equal(refreshed.models.DEFAULT, 'codex-2');
|
|
128
|
+
} finally {
|
|
129
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('provider model cache is persisted across service instances', async () => {
|
|
134
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-file-'));
|
|
135
|
+
const cachePath = path.join(tempRoot, 'models-cache.json');
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const writer = createProviderModelsService({
|
|
139
|
+
cachePath,
|
|
140
|
+
resolveProvider: () => ({
|
|
141
|
+
models: {
|
|
142
|
+
getSupportedModels: async () => createModels('gemini-cached'),
|
|
143
|
+
getCurrentActiveModel: async () => createCurrentActiveModel('gemini-active'),
|
|
144
|
+
changeActiveModel: async (input) => createSessionActiveModelChange('gemini', input),
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
await writer.getProviderModels('gemini');
|
|
149
|
+
|
|
150
|
+
const reader = createProviderModelsService({
|
|
151
|
+
cachePath,
|
|
152
|
+
resolveProvider: () => ({
|
|
153
|
+
models: {
|
|
154
|
+
getSupportedModels: async () => {
|
|
155
|
+
throw new Error('loader should not be called for persisted cache hits');
|
|
156
|
+
},
|
|
157
|
+
getCurrentActiveModel: async () => createCurrentActiveModel('gemini-active'),
|
|
158
|
+
changeActiveModel: async (input) => createSessionActiveModelChange('gemini', input),
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
const models = await reader.getProviderModels('gemini');
|
|
163
|
+
assert.equal(models.models.DEFAULT, 'gemini-cached');
|
|
164
|
+
assert.equal(models.cache.source, 'disk');
|
|
165
|
+
} finally {
|
|
166
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('concurrent provider model requests share one load operation', async () => {
|
|
171
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-pending-'));
|
|
172
|
+
let loadCount = 0;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const service = createProviderModelsService({
|
|
176
|
+
cachePath: path.join(tempRoot, 'models-cache.json'),
|
|
177
|
+
resolveProvider: () => ({
|
|
178
|
+
models: {
|
|
179
|
+
getSupportedModels: async () => {
|
|
180
|
+
loadCount += 1;
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
182
|
+
return createModels('claude-cached');
|
|
183
|
+
},
|
|
184
|
+
getCurrentActiveModel: async () => createCurrentActiveModel('claude-active'),
|
|
185
|
+
changeActiveModel: async (input) => createSessionActiveModelChange('claude', input),
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const [first, second] = await Promise.all([
|
|
191
|
+
service.getProviderModels('claude'),
|
|
192
|
+
service.getProviderModels('claude'),
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
assert.equal(loadCount, 1);
|
|
196
|
+
assert.equal(first.models.DEFAULT, 'claude-cached');
|
|
197
|
+
assert.equal(second.models.DEFAULT, 'claude-cached');
|
|
198
|
+
} finally {
|
|
199
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('bypassCache forces a fresh provider fetch and updates cache metadata', async () => {
|
|
204
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-refresh-'));
|
|
205
|
+
let currentTime = 1_000;
|
|
206
|
+
let loadCount = 0;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const service = createProviderModelsService({
|
|
210
|
+
cachePath: path.join(tempRoot, 'models-cache.json'),
|
|
211
|
+
now: () => currentTime,
|
|
212
|
+
resolveProvider: (provider) => ({
|
|
213
|
+
models: {
|
|
214
|
+
getSupportedModels: async () => {
|
|
215
|
+
loadCount += 1;
|
|
216
|
+
return createModels(`${provider}-${loadCount}`);
|
|
217
|
+
},
|
|
218
|
+
getCurrentActiveModel: async () => createCurrentActiveModel(`${provider}-active-${loadCount}`),
|
|
219
|
+
changeActiveModel: async (input) => createSessionActiveModelChange(provider, input),
|
|
220
|
+
},
|
|
221
|
+
}),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const first = await service.getProviderModels('claude');
|
|
225
|
+
currentTime += 50;
|
|
226
|
+
const refreshed = await service.getProviderModels('claude', { bypassCache: true });
|
|
227
|
+
|
|
228
|
+
assert.equal(first.models.DEFAULT, 'claude-1');
|
|
229
|
+
assert.equal(refreshed.models.DEFAULT, 'claude-2');
|
|
230
|
+
assert.equal(refreshed.cache.source, 'fresh');
|
|
231
|
+
assert.notEqual(refreshed.cache.updatedAt, first.cache.updatedAt);
|
|
232
|
+
assert.equal(loadCount, 2);
|
|
233
|
+
} finally {
|
|
234
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('provider models service delegates current active model lookups to the provider adapter', async () => {
|
|
239
|
+
const calls: Array<{ provider: LLMProvider; sessionId?: string }> = [];
|
|
240
|
+
const service = createProviderModelsService({
|
|
241
|
+
resolveProvider: (provider) => ({
|
|
242
|
+
models: {
|
|
243
|
+
getSupportedModels: async () => createModels(`${provider}-models`),
|
|
244
|
+
getCurrentActiveModel: async (sessionId) => {
|
|
245
|
+
calls.push({ provider, sessionId });
|
|
246
|
+
return createCurrentActiveModel(`${provider}-${sessionId}`);
|
|
247
|
+
},
|
|
248
|
+
changeActiveModel: async (input) => createSessionActiveModelChange(provider, input),
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const activeModel = await service.getCurrentActiveModel('opencode', 'session-123');
|
|
254
|
+
|
|
255
|
+
assert.deepEqual(calls, [{ provider: 'opencode', sessionId: 'session-123' }]);
|
|
256
|
+
assert.equal(activeModel.model, 'opencode-session-123');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('provider models service delegates active model change requests to the provider adapter', async () => {
|
|
260
|
+
const calls: Array<{ provider: LLMProvider; input: ProviderChangeActiveModelInput }> = [];
|
|
261
|
+
const service = createProviderModelsService({
|
|
262
|
+
resolveProvider: (provider) => ({
|
|
263
|
+
models: {
|
|
264
|
+
getSupportedModels: async () => createModels(`${provider}-models`),
|
|
265
|
+
getCurrentActiveModel: async () => createCurrentActiveModel(`${provider}-active`),
|
|
266
|
+
changeActiveModel: async (input) => {
|
|
267
|
+
calls.push({ provider, input });
|
|
268
|
+
return createSessionActiveModelChange(provider, input);
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const changedModel = await service.changeActiveModel('claude', {
|
|
275
|
+
sessionId: 'session-123',
|
|
276
|
+
model: 'opus',
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
assert.deepEqual(calls, [{
|
|
280
|
+
provider: 'claude',
|
|
281
|
+
input: {
|
|
282
|
+
sessionId: 'session-123',
|
|
283
|
+
model: 'opus',
|
|
284
|
+
},
|
|
285
|
+
}]);
|
|
286
|
+
assert.equal(changedModel.changed, true);
|
|
287
|
+
assert.equal(changedModel.model, 'opus');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('resolveResumeModel prefers a stored changed model over the requested one', async () => {
|
|
291
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-change-'));
|
|
292
|
+
const activeModelChangesPath = path.join(tempRoot, 'session-model-changes.json');
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const service = createProviderModelsService({
|
|
296
|
+
activeModelChangesPath,
|
|
297
|
+
resolveProvider: (provider) => ({
|
|
298
|
+
models: {
|
|
299
|
+
getSupportedModels: async () => createModels(`${provider}-models`),
|
|
300
|
+
getCurrentActiveModel: async () => createCurrentActiveModel(`${provider}-active`),
|
|
301
|
+
changeActiveModel: async (input) => createSessionActiveModelChange(provider, input),
|
|
302
|
+
},
|
|
303
|
+
}),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await writeProviderSessionActiveModelChange('cursor', {
|
|
307
|
+
sessionId: 'session-456',
|
|
308
|
+
model: 'composer-2',
|
|
309
|
+
}, {
|
|
310
|
+
filePath: activeModelChangesPath,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const model = await service.resolveResumeModel('cursor', 'session-456', 'composer-2-fast');
|
|
314
|
+
assert.equal(model, 'composer-2');
|
|
315
|
+
} finally {
|
|
316
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
317
|
+
}
|
|
318
|
+
});
|