@cloudcli-ai/cloudcli 1.37.0 → 1.37.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/dist/assets/index-D3jfzuKN.css +32 -0
- package/dist/assets/{index-eGqPeIEu.js → index-DTMWjJj8.js} +324 -305
- package/dist/index.html +2 -2
- package/dist-server/server/modules/commands/commands.routes.js +3 -3
- package/dist-server/server/modules/commands/commands.routes.js.map +1 -1
- package/dist-server/server/modules/commands/tests/commands.test.js +2 -7
- package/dist-server/server/modules/commands/tests/commands.test.js.map +1 -1
- package/dist-server/server/modules/database/index.js +2 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +18 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/provider-models.js +112 -0
- package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +63 -7
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +28 -3
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +102 -0
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -1
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +22 -0
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js +5 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.service.js +50 -18
- package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +83 -7
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +46 -14
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +8 -15
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +58 -71
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +1 -54
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +7 -7
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +32 -669
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +105 -328
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +0 -12
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +139 -31
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +167 -199
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-runtime.service.js +2 -2
- package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +12 -4
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +62 -3
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +53 -17
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +15 -126
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +5 -5
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +175 -292
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js +2 -6
- package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +1 -0
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js +126 -0
- package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +87 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +5 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/package.json +3 -3
- package/server/modules/commands/commands.routes.ts +3 -3
- package/server/modules/commands/tests/commands.test.ts +2 -7
- package/server/modules/database/index.ts +2 -0
- package/server/modules/database/migrations.ts +20 -0
- package/server/modules/database/repositories/provider-models.ts +168 -0
- package/server/modules/database/repositories/sessions.db.ts +85 -8
- package/server/modules/database/schema.ts +29 -3
- package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -0
- package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -1
- package/server/modules/database/tests/sessions.db.integration.test.ts +32 -0
- package/server/modules/file-tree/file-tree.module.ts +5 -1
- package/server/modules/file-tree/file-tree.service.ts +68 -20
- package/server/modules/file-tree/tests/file-tree.service.test.ts +104 -7
- package/server/modules/providers/list/claude/claude-models.provider.ts +46 -14
- package/server/modules/providers/list/claude/claude-runtime.provider.js +8 -16
- package/server/modules/providers/list/codex/codex-models.provider.ts +58 -99
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +1 -60
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +7 -7
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +32 -710
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +105 -413
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +0 -13
- package/server/modules/providers/provider.routes.ts +185 -33
- package/server/modules/providers/services/provider-models.service.ts +223 -288
- package/server/modules/providers/services/provider-runtime.service.ts +2 -3
- package/server/modules/providers/services/session-conversations-search.service.ts +12 -4
- package/server/modules/providers/services/sessions.service.ts +87 -3
- package/server/modules/providers/tests/codex-sessions.test.ts +57 -18
- package/server/modules/providers/tests/opencode-models.test.ts +34 -135
- package/server/modules/providers/tests/opencode-sessions.test.ts +5 -5
- package/server/modules/providers/tests/provider-models.service.test.ts +353 -450
- package/server/modules/providers/tests/provider-runtime.service.test.ts +2 -6
- package/server/modules/providers/tests/provider-token-usage.service.test.ts +1 -0
- package/server/modules/providers/tests/provider.routes.test.ts +183 -0
- package/server/modules/providers/tests/sessions.service.test.ts +127 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +5 -1
- package/server/shared/interfaces.ts +4 -6
- package/server/shared/types.ts +29 -17
- package/dist/assets/index-DSlds633.css +0 -32
|
@@ -1,450 +1,353 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
test('
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
assert.
|
|
278
|
-
assert.equal(resolved.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
assert.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
assert.equal(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
assert.equal(
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
test('
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
assert.equal(resolved.model, 'provider-reported');
|
|
356
|
-
assert.equal(resolved.source, 'provider');
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
test('resolveSessionModel uses the requested model when the provider only reports its catalog default', async () => {
|
|
360
|
-
const service = createProviderModelsService({
|
|
361
|
-
cachePath: createEphemeralCachePath(),
|
|
362
|
-
sessions: createSessionStore({ 'session-1': null }),
|
|
363
|
-
resolveProvider: () => ({
|
|
364
|
-
models: {
|
|
365
|
-
getSupportedModels: async () => createModels('default'),
|
|
366
|
-
getCurrentActiveModel: async () => createCurrentActiveModel('default'),
|
|
367
|
-
},
|
|
368
|
-
}),
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
const resolved = await service.resolveSessionModel('claude', {
|
|
372
|
-
sessionId: 'session-1',
|
|
373
|
-
requestedModel: 'haiku',
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
assert.equal(resolved.model, 'haiku');
|
|
377
|
-
assert.equal(resolved.source, 'session');
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
test('resolveSessionModel answers with the requested model for a chat that has no session yet', async () => {
|
|
381
|
-
const service = createProviderModelsService({
|
|
382
|
-
sessions: createSessionStore(),
|
|
383
|
-
resolveProvider: (provider) => ({
|
|
384
|
-
models: {
|
|
385
|
-
getSupportedModels: async () => createModels(`${provider}-models`),
|
|
386
|
-
getCurrentActiveModel: async () => createCurrentActiveModel('provider-reported'),
|
|
387
|
-
},
|
|
388
|
-
}),
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
const resolved = await service.resolveSessionModel('codex', { requestedModel: 'gpt-5.5' });
|
|
392
|
-
|
|
393
|
-
assert.equal(resolved.model, 'gpt-5.5');
|
|
394
|
-
assert.equal(resolved.sessionId, null);
|
|
395
|
-
assert.equal(resolved.source, 'session');
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
test('resolveSessionModel falls back to the catalog default with nothing else to go on', async () => {
|
|
399
|
-
const service = createProviderModelsService({
|
|
400
|
-
cachePath: createEphemeralCachePath(),
|
|
401
|
-
sessions: createSessionStore(),
|
|
402
|
-
resolveProvider: (provider) => ({
|
|
403
|
-
models: {
|
|
404
|
-
getSupportedModels: async () => createModels(`${provider}-models`),
|
|
405
|
-
getCurrentActiveModel: async () => createCurrentActiveModel('provider-reported'),
|
|
406
|
-
},
|
|
407
|
-
}),
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
const resolved = await service.resolveSessionModel('codex');
|
|
411
|
-
|
|
412
|
-
assert.equal(resolved.model, 'codex-models');
|
|
413
|
-
assert.equal(resolved.source, 'default');
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
test('resolveResumeModel prefers the recorded session model over the requested one', async () => {
|
|
417
|
-
const service = createProviderModelsService({
|
|
418
|
-
sessions: createSessionStore({ 'session-456': 'composer-2' }),
|
|
419
|
-
resolveProvider: (provider) => ({
|
|
420
|
-
models: {
|
|
421
|
-
getSupportedModels: async () => createModels(`${provider}-models`),
|
|
422
|
-
getCurrentActiveModel: async () => createCurrentActiveModel(`${provider}-active`),
|
|
423
|
-
},
|
|
424
|
-
}),
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
const model = await service.resolveResumeModel('cursor', 'session-456', 'composer-2-fast');
|
|
428
|
-
assert.equal(model, 'composer-2');
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
test('resolveResumeModel never lets provider session state override the requested model', async () => {
|
|
432
|
-
let providerLookups = 0;
|
|
433
|
-
const service = createProviderModelsService({
|
|
434
|
-
sessions: createSessionStore({ 'session-456': null }),
|
|
435
|
-
resolveProvider: (provider) => ({
|
|
436
|
-
models: {
|
|
437
|
-
getSupportedModels: async () => createModels(`${provider}-models`),
|
|
438
|
-
getCurrentActiveModel: async () => {
|
|
439
|
-
providerLookups += 1;
|
|
440
|
-
return createCurrentActiveModel('global-config-model');
|
|
441
|
-
},
|
|
442
|
-
},
|
|
443
|
-
}),
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
const model = await service.resolveResumeModel('codex', 'session-456', 'gpt-5.5');
|
|
447
|
-
|
|
448
|
-
assert.equal(model, 'gpt-5.5');
|
|
449
|
-
assert.equal(providerLookups, 0);
|
|
450
|
-
});
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { createProviderModelsService } from '@/modules/providers/services/provider-models.service.js';
|
|
5
|
+
import type {
|
|
6
|
+
CustomProviderModelInput,
|
|
7
|
+
CustomProviderModelRecord,
|
|
8
|
+
LLMProvider,
|
|
9
|
+
ProviderCurrentActiveModel,
|
|
10
|
+
ProviderModelsDefinition,
|
|
11
|
+
} from '@/shared/types.js';
|
|
12
|
+
import { AppError } from '@/shared/utils.js';
|
|
13
|
+
|
|
14
|
+
const createModels = (value: string): ProviderModelsDefinition => ({
|
|
15
|
+
OPTIONS: [{ value, label: value }],
|
|
16
|
+
DEFAULT: value,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const createCurrentActiveModel = (model: string): ProviderCurrentActiveModel => ({ model });
|
|
20
|
+
|
|
21
|
+
/** In-memory stand-in for the `sessions` table rows the service reads and writes. */
|
|
22
|
+
const createSessionStore = (
|
|
23
|
+
rows: Record<string, string | null> = {},
|
|
24
|
+
efforts: Record<string, string | null> = {},
|
|
25
|
+
) => {
|
|
26
|
+
const sessions = new Map(Object.entries(rows).map(([sessionId, model]) => [
|
|
27
|
+
sessionId,
|
|
28
|
+
{ model, effort: efforts[sessionId] ?? null },
|
|
29
|
+
]));
|
|
30
|
+
return {
|
|
31
|
+
sessions,
|
|
32
|
+
getSessionById: (sessionId: string) =>
|
|
33
|
+
sessions.get(sessionId) ?? null,
|
|
34
|
+
setSessionModel: (sessionId: string, model: string) => {
|
|
35
|
+
const session = sessions.get(sessionId);
|
|
36
|
+
if (session) {
|
|
37
|
+
session.model = model;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
setSessionEffort: (sessionId: string, effort: string) => {
|
|
41
|
+
const session = sessions.get(sessionId);
|
|
42
|
+
if (session) {
|
|
43
|
+
session.effort = effort;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const createCatalogStore = () => {
|
|
50
|
+
const rows = new Map<LLMProvider, CustomProviderModelRecord[]>();
|
|
51
|
+
let nextRecordId = 1;
|
|
52
|
+
const readRows = (provider: LLMProvider) => rows.get(provider) ?? [];
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
rows,
|
|
56
|
+
listCustomProviderModels(provider: LLMProvider) {
|
|
57
|
+
return [...readRows(provider)];
|
|
58
|
+
},
|
|
59
|
+
getCustomProviderModel(provider: LLMProvider, recordId: number) {
|
|
60
|
+
return readRows(provider).find((record) => record.recordId === recordId) ?? null;
|
|
61
|
+
},
|
|
62
|
+
findCustomProviderModelByModelId(provider: LLMProvider, modelId: string) {
|
|
63
|
+
return readRows(provider).find((record) => record.modelId === modelId) ?? null;
|
|
64
|
+
},
|
|
65
|
+
createCustomProviderModel(provider: LLMProvider, input: CustomProviderModelInput) {
|
|
66
|
+
const record: CustomProviderModelRecord = {
|
|
67
|
+
recordId: nextRecordId++,
|
|
68
|
+
provider,
|
|
69
|
+
modelId: input.id,
|
|
70
|
+
model: input.model,
|
|
71
|
+
sortOrder: readRows(provider).length,
|
|
72
|
+
};
|
|
73
|
+
rows.set(provider, [...readRows(provider), record]);
|
|
74
|
+
return record;
|
|
75
|
+
},
|
|
76
|
+
updateCustomProviderModel(
|
|
77
|
+
provider: LLMProvider,
|
|
78
|
+
recordId: number,
|
|
79
|
+
input: CustomProviderModelInput,
|
|
80
|
+
) {
|
|
81
|
+
const existing = readRows(provider).find((record) => record.recordId === recordId);
|
|
82
|
+
if (!existing) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const updated = { ...existing, modelId: input.id, model: input.model };
|
|
86
|
+
rows.set(provider, readRows(provider).map((record) => (
|
|
87
|
+
record.recordId === recordId ? updated : record
|
|
88
|
+
)));
|
|
89
|
+
return updated;
|
|
90
|
+
},
|
|
91
|
+
deleteCustomProviderModel(provider: LLMProvider, recordId: number, _fallbackModelId: string) {
|
|
92
|
+
const existing = readRows(provider).find((record) => record.recordId === recordId);
|
|
93
|
+
if (!existing) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
rows.set(provider, readRows(provider).filter((record) => record.recordId !== recordId));
|
|
97
|
+
return existing;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const createTestService = (options: {
|
|
103
|
+
catalog?: ReturnType<typeof createCatalogStore>;
|
|
104
|
+
sessions?: ReturnType<typeof createSessionStore>;
|
|
105
|
+
activeModel?: (provider: LLMProvider, sessionId?: string) => string;
|
|
106
|
+
onCatalogRead?: (provider: LLMProvider) => void;
|
|
107
|
+
} = {}) => {
|
|
108
|
+
const catalog = options.catalog ?? createCatalogStore();
|
|
109
|
+
const sessions = options.sessions ?? createSessionStore();
|
|
110
|
+
const service = createProviderModelsService({
|
|
111
|
+
catalog,
|
|
112
|
+
sessions,
|
|
113
|
+
resolveProvider: (provider) => ({
|
|
114
|
+
models: {
|
|
115
|
+
getSupportedModels: async () => {
|
|
116
|
+
options.onCatalogRead?.(provider);
|
|
117
|
+
return createModels(`${provider}-default`);
|
|
118
|
+
},
|
|
119
|
+
getCurrentActiveModel: async (sessionId) => createCurrentActiveModel(
|
|
120
|
+
options.activeModel?.(provider, sessionId) ?? `${provider}-default`,
|
|
121
|
+
),
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return { service, catalog, sessions };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
test('provider catalogs merge source-controlled defaults with custom persistence rows', async () => {
|
|
130
|
+
const calls: LLMProvider[] = [];
|
|
131
|
+
const { service, catalog } = createTestService({ onCatalogRead: (provider) => calls.push(provider) });
|
|
132
|
+
|
|
133
|
+
const models = await service.getProviderModels('codex');
|
|
134
|
+
|
|
135
|
+
assert.deepEqual(calls, ['codex']);
|
|
136
|
+
assert.equal(models.DEFAULT, 'codex-default');
|
|
137
|
+
assert.deepEqual(models.OPTIONS[0], {
|
|
138
|
+
value: 'codex-default',
|
|
139
|
+
label: 'codex-default',
|
|
140
|
+
isCustom: false,
|
|
141
|
+
});
|
|
142
|
+
assert.deepEqual(catalog.rows.get('codex'), undefined);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('custom models can be created, edited, and deleted', async () => {
|
|
146
|
+
const { service } = createTestService();
|
|
147
|
+
const created = await service.createCustomModel('claude', {
|
|
148
|
+
model: 'My Claude',
|
|
149
|
+
id: 'claude-my-model',
|
|
150
|
+
});
|
|
151
|
+
const recordId = created.model.recordId as number;
|
|
152
|
+
|
|
153
|
+
assert.equal(created.model.isCustom, true);
|
|
154
|
+
assert.equal(created.models.OPTIONS.at(-1)?.value, 'claude-my-model');
|
|
155
|
+
|
|
156
|
+
const updated = await service.updateCustomModel('claude', recordId, {
|
|
157
|
+
model: 'My Better Claude',
|
|
158
|
+
id: 'claude-my-model-v2',
|
|
159
|
+
});
|
|
160
|
+
assert.equal(updated.model.label, 'My Better Claude');
|
|
161
|
+
assert.equal(updated.model.value, 'claude-my-model-v2');
|
|
162
|
+
|
|
163
|
+
const removed = await service.deleteCustomModel('claude', recordId);
|
|
164
|
+
assert.equal(removed.model.value, 'claude-my-model-v2');
|
|
165
|
+
assert.equal(removed.models.OPTIONS.some((option) => option.recordId === recordId), false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('duplicate model ids are rejected within one provider', async () => {
|
|
169
|
+
const { service } = createTestService();
|
|
170
|
+
await service.createCustomModel('cursor', { model: 'First', id: 'custom-id' });
|
|
171
|
+
|
|
172
|
+
await assert.rejects(
|
|
173
|
+
() => service.createCustomModel('cursor', { model: 'Second', id: 'custom-id' }),
|
|
174
|
+
(error) => error instanceof AppError
|
|
175
|
+
&& error.code === 'MODEL_ID_ALREADY_EXISTS'
|
|
176
|
+
&& error.statusCode === 409,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await assert.rejects(
|
|
180
|
+
() => service.createCustomModel('cursor', {
|
|
181
|
+
model: 'Duplicate built-in',
|
|
182
|
+
id: 'cursor-default',
|
|
183
|
+
}),
|
|
184
|
+
(error) => error instanceof AppError
|
|
185
|
+
&& error.code === 'MODEL_ID_ALREADY_EXISTS'
|
|
186
|
+
&& error.statusCode === 409,
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('predefined models have no database record or mutation target', async () => {
|
|
191
|
+
const { service, catalog } = createTestService();
|
|
192
|
+
const models = await service.getProviderModels('opencode');
|
|
193
|
+
assert.equal(models.OPTIONS[0]?.recordId, undefined);
|
|
194
|
+
assert.equal(models.OPTIONS[0]?.isCustom, false);
|
|
195
|
+
assert.deepEqual(catalog.rows.get('opencode'), undefined);
|
|
196
|
+
|
|
197
|
+
await assert.rejects(
|
|
198
|
+
() => service.updateCustomModel('opencode', 999, { model: 'Changed', id: 'changed' }),
|
|
199
|
+
(error) => error instanceof AppError && error.code === 'MODEL_NOT_FOUND',
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('resolveSessionModel asks the provider adapter for the requested session', async () => {
|
|
204
|
+
const calls: Array<{ provider: LLMProvider; sessionId?: string }> = [];
|
|
205
|
+
const { service } = createTestService({
|
|
206
|
+
sessions: createSessionStore({ 'session-123': null }),
|
|
207
|
+
activeModel: (provider, sessionId) => {
|
|
208
|
+
calls.push({ provider, sessionId });
|
|
209
|
+
return `${provider}-${sessionId}`;
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const resolved = await service.resolveSessionModel('opencode', { sessionId: 'session-123' });
|
|
214
|
+
|
|
215
|
+
assert.deepEqual(calls, [{ provider: 'opencode', sessionId: 'session-123' }]);
|
|
216
|
+
assert.equal(resolved.model, 'opencode-session-123');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('setSessionModel records the model on the session row', () => {
|
|
220
|
+
const sessions = createSessionStore({ 'session-1': null });
|
|
221
|
+
const { service } = createTestService({ sessions });
|
|
222
|
+
|
|
223
|
+
const stored = service.setSessionModel('claude', 'session-1', 'opus');
|
|
224
|
+
|
|
225
|
+
assert.deepEqual(stored, {
|
|
226
|
+
provider: 'claude',
|
|
227
|
+
sessionId: 'session-1',
|
|
228
|
+
model: 'opus',
|
|
229
|
+
effort: null,
|
|
230
|
+
source: 'session',
|
|
231
|
+
});
|
|
232
|
+
assert.equal(sessions.sessions.get('session-1')?.model, 'opus');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('setSessionModel ignores sessions that have no row yet', () => {
|
|
236
|
+
const sessions = createSessionStore();
|
|
237
|
+
const { service } = createTestService({ sessions });
|
|
238
|
+
|
|
239
|
+
assert.equal(service.setSessionModel('claude', 'missing-session', 'opus'), null);
|
|
240
|
+
assert.equal(sessions.sessions.size, 0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('setSessionEffort records an explicit effort on the session row', () => {
|
|
244
|
+
const sessions = createSessionStore({ 'session-1': 'gpt-5.6-sol' });
|
|
245
|
+
const { service } = createTestService({ sessions });
|
|
246
|
+
|
|
247
|
+
const stored = service.setSessionEffort('codex', 'session-1', 'ultra');
|
|
248
|
+
|
|
249
|
+
assert.deepEqual(stored, {
|
|
250
|
+
provider: 'codex',
|
|
251
|
+
sessionId: 'session-1',
|
|
252
|
+
effort: 'ultra',
|
|
253
|
+
source: 'session',
|
|
254
|
+
});
|
|
255
|
+
assert.equal(sessions.sessions.get('session-1')?.effort, 'ultra');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('setSessionEffort ignores sessions that have no row yet', () => {
|
|
259
|
+
const sessions = createSessionStore();
|
|
260
|
+
const { service } = createTestService({ sessions });
|
|
261
|
+
|
|
262
|
+
assert.equal(service.setSessionEffort('codex', 'missing-session', 'high'), null);
|
|
263
|
+
assert.equal(sessions.sessions.size, 0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('resolveSessionModel prefers the recorded session model', async () => {
|
|
267
|
+
const { service } = createTestService({
|
|
268
|
+
sessions: createSessionStore({ 'session-1': 'haiku' }, { 'session-1': 'high' }),
|
|
269
|
+
activeModel: () => 'provider-reported',
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const resolved = await service.resolveSessionModel('claude', {
|
|
273
|
+
sessionId: 'session-1',
|
|
274
|
+
requestedModel: 'sonnet',
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
assert.equal(resolved.model, 'haiku');
|
|
278
|
+
assert.equal(resolved.effort, 'high');
|
|
279
|
+
assert.equal(resolved.source, 'session');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('resolveSessionModel uses provider session state for unrecorded external sessions', async () => {
|
|
283
|
+
const { service } = createTestService({
|
|
284
|
+
sessions: createSessionStore({ 'session-1': null }),
|
|
285
|
+
activeModel: () => 'provider-reported',
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const resolved = await service.resolveSessionModel('opencode', {
|
|
289
|
+
sessionId: 'session-1',
|
|
290
|
+
requestedModel: 'requested',
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
assert.equal(resolved.model, 'provider-reported');
|
|
294
|
+
assert.equal(resolved.source, 'provider');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('resolveSessionModel uses the requested model when provider reports the catalog default', async () => {
|
|
298
|
+
const { service } = createTestService({
|
|
299
|
+
sessions: createSessionStore({ 'session-1': null }),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const resolved = await service.resolveSessionModel('claude', {
|
|
303
|
+
sessionId: 'session-1',
|
|
304
|
+
requestedModel: 'haiku',
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
assert.equal(resolved.model, 'haiku');
|
|
308
|
+
assert.equal(resolved.source, 'session');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('resolveSessionModel returns a requested model before a session exists', async () => {
|
|
312
|
+
const { service } = createTestService();
|
|
313
|
+
|
|
314
|
+
const resolved = await service.resolveSessionModel('codex', { requestedModel: 'gpt-5.5' });
|
|
315
|
+
|
|
316
|
+
assert.equal(resolved.model, 'gpt-5.5');
|
|
317
|
+
assert.equal(resolved.sessionId, null);
|
|
318
|
+
assert.equal(resolved.source, 'session');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('resolveSessionModel falls back to the provider adapter default', async () => {
|
|
322
|
+
const { service } = createTestService();
|
|
323
|
+
|
|
324
|
+
const resolved = await service.resolveSessionModel('codex');
|
|
325
|
+
|
|
326
|
+
assert.equal(resolved.model, 'codex-default');
|
|
327
|
+
assert.equal(resolved.source, 'default');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('resolveResumeModel prefers the recorded session model over the requested one', async () => {
|
|
331
|
+
const { service } = createTestService({
|
|
332
|
+
sessions: createSessionStore({ 'session-456': 'composer-2' }),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const model = await service.resolveResumeModel('cursor', 'session-456', 'composer-2-fast');
|
|
336
|
+
assert.equal(model, 'composer-2');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('resolveResumeModel never consults provider-global state', async () => {
|
|
340
|
+
let providerLookups = 0;
|
|
341
|
+
const { service } = createTestService({
|
|
342
|
+
sessions: createSessionStore({ 'session-456': null }),
|
|
343
|
+
activeModel: () => {
|
|
344
|
+
providerLookups += 1;
|
|
345
|
+
return 'global-config-model';
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const model = await service.resolveResumeModel('codex', 'session-456', 'gpt-5.5');
|
|
350
|
+
|
|
351
|
+
assert.equal(model, 'gpt-5.5');
|
|
352
|
+
assert.equal(providerLookups, 0);
|
|
353
|
+
});
|