@cloudcli-ai/cloudcli 1.32.0 → 1.33.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.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-BsyL_xmI.js +1348 -0
- package/dist/assets/index-FSP9Hz7V.css +32 -0
- package/dist/index.html +2 -2
- package/dist/modelConstants.js +848 -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 +234 -67
- 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-auth.provider.js +28 -6
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +146 -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 +226 -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 +262 -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 +298 -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 +12 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +4 -4
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +18 -3
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js +6 -2
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/websocket-server.service.js +18 -0
- package/dist-server/server/modules/websocket/services/websocket-server.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 +5 -2
- package/public/api-docs.html +878 -0
- package/public/modelConstants.js +848 -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 +259 -75
- 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-auth.provider.ts +34 -6
- package/server/modules/providers/list/claude/claude-models.provider.ts +196 -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 +157 -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 +358 -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 +334 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +349 -0
- package/server/modules/providers/tests/skills.test.ts +66 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +15 -1
- package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +4 -4
- package/server/modules/websocket/services/shell-websocket.service.ts +22 -2
- package/server/modules/websocket/services/websocket-auth.service.ts +7 -2
- package/server/modules/websocket/services/websocket-server.service.ts +18 -0
- package/server/openai-codex.js +41 -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-AaSE7-3t.js +0 -1336
- package/dist/assets/index-CKaku72N.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
|
@@ -2,10 +2,17 @@ import express, { type Request, type Response } from 'express';
|
|
|
2
2
|
|
|
3
3
|
import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
|
|
4
4
|
import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
|
|
5
|
+
import { providerModelsService } from '@/modules/providers/services/provider-models.service.js';
|
|
5
6
|
import { providerSkillsService } from '@/modules/providers/services/skills.service.js';
|
|
6
7
|
import { sessionConversationsSearchService } from '@/modules/providers/services/session-conversations-search.service.js';
|
|
7
8
|
import { sessionsService } from '@/modules/providers/services/sessions.service.js';
|
|
8
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
LLMProvider,
|
|
11
|
+
McpScope,
|
|
12
|
+
McpTransport,
|
|
13
|
+
ProviderChangeActiveModelInput,
|
|
14
|
+
UpsertProviderMcpServerInput,
|
|
15
|
+
} from '@/shared/types.js';
|
|
9
16
|
import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
|
|
10
17
|
|
|
11
18
|
const router = express.Router();
|
|
@@ -173,7 +180,13 @@ const parseMcpUpsertPayload = (payload: unknown): UpsertProviderMcpServerInput =
|
|
|
173
180
|
|
|
174
181
|
const parseProvider = (value: unknown): LLMProvider => {
|
|
175
182
|
const normalized = normalizeProviderParam(value);
|
|
176
|
-
if (
|
|
183
|
+
if (
|
|
184
|
+
normalized === 'claude'
|
|
185
|
+
|| normalized === 'codex'
|
|
186
|
+
|| normalized === 'cursor'
|
|
187
|
+
|| normalized === 'gemini'
|
|
188
|
+
|| normalized === 'opencode'
|
|
189
|
+
) {
|
|
177
190
|
return normalized;
|
|
178
191
|
}
|
|
179
192
|
|
|
@@ -239,6 +252,29 @@ const parseSessionSearchLimit = (value: unknown): number => {
|
|
|
239
252
|
return Math.max(1, Math.min(parsed, 100));
|
|
240
253
|
};
|
|
241
254
|
|
|
255
|
+
const parseChangeActiveModelPayload = (payload: unknown): ProviderChangeActiveModelInput => {
|
|
256
|
+
if (!payload || typeof payload !== 'object') {
|
|
257
|
+
throw new AppError('Request body must be an object.', {
|
|
258
|
+
code: 'INVALID_REQUEST_BODY',
|
|
259
|
+
statusCode: 400,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const body = payload as Record<string, unknown>;
|
|
264
|
+
const model = readOptionalQueryString(body.model);
|
|
265
|
+
if (!model) {
|
|
266
|
+
throw new AppError('model is required.', {
|
|
267
|
+
code: 'MODEL_REQUIRED',
|
|
268
|
+
statusCode: 400,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
sessionId: '',
|
|
274
|
+
model,
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
|
|
242
278
|
router.get(
|
|
243
279
|
'/:provider/auth/status',
|
|
244
280
|
asyncHandler(async (req: Request, res: Response) => {
|
|
@@ -248,6 +284,30 @@ router.get(
|
|
|
248
284
|
}),
|
|
249
285
|
);
|
|
250
286
|
|
|
287
|
+
router.get(
|
|
288
|
+
'/:provider/models',
|
|
289
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
290
|
+
const provider = parseProvider(req.params.provider);
|
|
291
|
+
const bypassCache = parseOptionalBooleanQuery(req.query.bypassCache, 'bypassCache') ?? false;
|
|
292
|
+
const result = await providerModelsService.getProviderModels(provider, { bypassCache });
|
|
293
|
+
res.json(createApiSuccessResponse({ provider, models: result.models, cache: result.cache }));
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
router.post(
|
|
298
|
+
'/:provider/sessions/:sessionId/active-model',
|
|
299
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
300
|
+
const provider = parseProvider(req.params.provider);
|
|
301
|
+
const sessionId = parseSessionId(req.params.sessionId);
|
|
302
|
+
const payload = parseChangeActiveModelPayload(req.body);
|
|
303
|
+
const result = await providerModelsService.changeActiveModel(provider, {
|
|
304
|
+
...payload,
|
|
305
|
+
sessionId,
|
|
306
|
+
});
|
|
307
|
+
res.json(createApiSuccessResponse(result));
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
|
|
251
311
|
// ----------------- Skills routes -----------------
|
|
252
312
|
router.get(
|
|
253
313
|
'/:provider/skills',
|
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
|
|
3
1
|
import { providerRegistry } from '@/modules/providers/provider.registry.js';
|
|
4
2
|
import type { LLMProvider, McpScope, ProviderMcpServer, UpsertProviderMcpServerInput } from '@/shared/types.js';
|
|
5
3
|
import { AppError } from '@/shared/utils.js';
|
|
6
4
|
|
|
7
|
-
/** Cursor MCP is not supported on Windows hosts (no Cursor CLI integration). */
|
|
8
|
-
function includeProviderInGlobalMcp(providerId: LLMProvider): boolean {
|
|
9
|
-
if (providerId === 'cursor' && os.platform() === 'win32') {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
5
|
|
|
17
6
|
export const providerMcpService = {
|
|
18
7
|
/**
|
|
@@ -75,7 +64,7 @@ export const providerMcpService = {
|
|
|
75
64
|
|
|
76
65
|
const scope = input.scope ?? 'project';
|
|
77
66
|
const results: Array<{ provider: LLMProvider; created: boolean; error?: string }> = [];
|
|
78
|
-
const providers = providerRegistry.listProviders()
|
|
67
|
+
const providers = providerRegistry.listProviders();
|
|
79
68
|
for (const provider of providers) {
|
|
80
69
|
try {
|
|
81
70
|
await provider.mcp.upsertServer({ ...input, scope });
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { providerRegistry } from '@/modules/providers/provider.registry.js';
|
|
6
|
+
import type { IProvider } from '@/shared/interfaces.js';
|
|
7
|
+
import type {
|
|
8
|
+
LLMProvider,
|
|
9
|
+
ProviderChangeActiveModelInput,
|
|
10
|
+
ProviderCurrentActiveModel,
|
|
11
|
+
ProviderModelsCacheInfo,
|
|
12
|
+
ProviderModelsDefinition,
|
|
13
|
+
ProviderModelsResult,
|
|
14
|
+
ProviderSessionActiveModelChange,
|
|
15
|
+
} from '@/shared/types.js';
|
|
16
|
+
import { readProviderSessionActiveModelChange } from '@/shared/utils.js';
|
|
17
|
+
|
|
18
|
+
export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
|
|
19
|
+
const PROVIDER_MODELS_CACHE_VERSION = 1;
|
|
20
|
+
const UNCACHED_PROVIDERS = new Set<LLMProvider>(['claude']);
|
|
21
|
+
|
|
22
|
+
type ProviderModelsServiceDependencies = {
|
|
23
|
+
resolveProvider?: (provider: LLMProvider) => Pick<IProvider, 'models'>;
|
|
24
|
+
cachePath?: string;
|
|
25
|
+
activeModelChangesPath?: string;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type ProviderModelsOptions = {
|
|
30
|
+
bypassCache?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ProviderModelsCacheEntry = {
|
|
34
|
+
updatedAt: number;
|
|
35
|
+
expiresAt: number;
|
|
36
|
+
models: ProviderModelsDefinition;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type ProviderModelsCacheFile = {
|
|
40
|
+
version: number;
|
|
41
|
+
entries: Record<string, ProviderModelsCacheEntry>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getProviderModelsCachePath = (): string => path.join(
|
|
45
|
+
os.homedir(),
|
|
46
|
+
'.cloudcli',
|
|
47
|
+
'provider-models-cache.json',
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const toProviderModelsCacheInfo = (
|
|
51
|
+
entry: ProviderModelsCacheEntry,
|
|
52
|
+
source: ProviderModelsCacheInfo['source'],
|
|
53
|
+
): ProviderModelsCacheInfo => ({
|
|
54
|
+
updatedAt: new Date(entry.updatedAt).toISOString(),
|
|
55
|
+
expiresAt: new Date(entry.expiresAt).toISOString(),
|
|
56
|
+
source,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const isProviderModelOption = (
|
|
60
|
+
value: unknown,
|
|
61
|
+
): value is ProviderModelsDefinition['OPTIONS'][number] => (
|
|
62
|
+
Boolean(value)
|
|
63
|
+
&& typeof value === 'object'
|
|
64
|
+
&& typeof (value as ProviderModelsDefinition['OPTIONS'][number]).value === 'string'
|
|
65
|
+
&& typeof (value as ProviderModelsDefinition['OPTIONS'][number]).label === 'string'
|
|
66
|
+
&& (
|
|
67
|
+
typeof (value as ProviderModelsDefinition['OPTIONS'][number]).description === 'undefined'
|
|
68
|
+
|| typeof (value as ProviderModelsDefinition['OPTIONS'][number]).description === 'string'
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const isProviderModelsDefinition = (value: unknown): value is ProviderModelsDefinition => (
|
|
73
|
+
Boolean(value)
|
|
74
|
+
&& typeof value === 'object'
|
|
75
|
+
&& Array.isArray((value as ProviderModelsDefinition).OPTIONS)
|
|
76
|
+
&& (value as ProviderModelsDefinition).OPTIONS.every(isProviderModelOption)
|
|
77
|
+
&& typeof (value as ProviderModelsDefinition).DEFAULT === 'string'
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const isProviderModelsCacheEntry = (value: unknown): value is ProviderModelsCacheEntry => (
|
|
81
|
+
Boolean(value)
|
|
82
|
+
&& typeof value === 'object'
|
|
83
|
+
&& typeof (value as ProviderModelsCacheEntry).updatedAt === 'number'
|
|
84
|
+
&& typeof (value as ProviderModelsCacheEntry).expiresAt === 'number'
|
|
85
|
+
&& isProviderModelsDefinition((value as ProviderModelsCacheEntry).models)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const readProviderModelsCacheFile = async (
|
|
89
|
+
cachePath: string,
|
|
90
|
+
): Promise<ProviderModelsCacheFile | null> => {
|
|
91
|
+
try {
|
|
92
|
+
const raw = await readFile(cachePath, 'utf8');
|
|
93
|
+
const parsed = JSON.parse(raw) as Partial<ProviderModelsCacheFile>;
|
|
94
|
+
if (parsed.version !== PROVIDER_MODELS_CACHE_VERSION || !parsed.entries || typeof parsed.entries !== 'object') {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const entries = Object.fromEntries(
|
|
99
|
+
Object.entries(parsed.entries).filter((entry): entry is [string, ProviderModelsCacheEntry] =>
|
|
100
|
+
isProviderModelsCacheEntry(entry[1]),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
version: PROVIDER_MODELS_CACHE_VERSION,
|
|
106
|
+
entries,
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const writeProviderModelsCacheFile = async (
|
|
114
|
+
cachePath: string,
|
|
115
|
+
entries: Map<LLMProvider, ProviderModelsCacheEntry>,
|
|
116
|
+
now: number,
|
|
117
|
+
): Promise<void> => {
|
|
118
|
+
const serializableEntries = Object.fromEntries(
|
|
119
|
+
[...entries.entries()].filter(([, entry]) => entry.expiresAt > now),
|
|
120
|
+
);
|
|
121
|
+
const payload: ProviderModelsCacheFile = {
|
|
122
|
+
version: PROVIDER_MODELS_CACHE_VERSION,
|
|
123
|
+
entries: serializableEntries,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
127
|
+
await writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Provider model lookup service.
|
|
132
|
+
*
|
|
133
|
+
* Routes and other service callers use this layer instead of resolving provider
|
|
134
|
+
* classes directly so the provider-registry dependency stays centralized in one
|
|
135
|
+
* place.
|
|
136
|
+
*/
|
|
137
|
+
export const createProviderModelsService = (dependencies: ProviderModelsServiceDependencies = {}) => {
|
|
138
|
+
const resolveProvider = dependencies.resolveProvider ?? providerRegistry.resolveProvider;
|
|
139
|
+
const cachePath = dependencies.cachePath ?? getProviderModelsCachePath();
|
|
140
|
+
const activeModelChangesPath = dependencies.activeModelChangesPath;
|
|
141
|
+
const now = dependencies.now ?? (() => Date.now());
|
|
142
|
+
const memoryCache = new Map<LLMProvider, ProviderModelsCacheEntry>();
|
|
143
|
+
const pendingRequests = new Map<LLMProvider, Promise<ProviderModelsResult>>();
|
|
144
|
+
let persistedCacheLoaded = false;
|
|
145
|
+
let persistedCacheLoadPromise: Promise<void> | null = null;
|
|
146
|
+
|
|
147
|
+
const pruneExpiredMemoryEntry = (
|
|
148
|
+
provider: LLMProvider,
|
|
149
|
+
currentTime: number,
|
|
150
|
+
source: ProviderModelsCacheInfo['source'],
|
|
151
|
+
): ProviderModelsResult | null => {
|
|
152
|
+
const cachedEntry = memoryCache.get(provider);
|
|
153
|
+
if (!cachedEntry) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (cachedEntry.expiresAt > currentTime) {
|
|
158
|
+
return {
|
|
159
|
+
models: cachedEntry.models,
|
|
160
|
+
cache: toProviderModelsCacheInfo(cachedEntry, source),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
memoryCache.delete(provider);
|
|
165
|
+
return null;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const loadPersistedCache = async (): Promise<void> => {
|
|
169
|
+
if (persistedCacheLoaded) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!persistedCacheLoadPromise) {
|
|
174
|
+
persistedCacheLoadPromise = (async () => {
|
|
175
|
+
const cacheFile = await readProviderModelsCacheFile(cachePath);
|
|
176
|
+
const currentTime = now();
|
|
177
|
+
|
|
178
|
+
for (const [provider, entry] of Object.entries(cacheFile?.entries ?? {})) {
|
|
179
|
+
if (entry.expiresAt > currentTime) {
|
|
180
|
+
memoryCache.set(provider as LLMProvider, entry);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
persistedCacheLoaded = true;
|
|
185
|
+
})().finally(() => {
|
|
186
|
+
persistedCacheLoadPromise = null;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await persistedCacheLoadPromise;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const persistCache = async (): Promise<void> => {
|
|
194
|
+
try {
|
|
195
|
+
await writeProviderModelsCacheFile(cachePath, memoryCache, now());
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.warn('Unable to persist provider models cache:', error);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const setCacheEntry = async (
|
|
202
|
+
provider: LLMProvider,
|
|
203
|
+
models: ProviderModelsDefinition,
|
|
204
|
+
): Promise<ProviderModelsCacheEntry> => {
|
|
205
|
+
const currentTime = now();
|
|
206
|
+
const entry: ProviderModelsCacheEntry = {
|
|
207
|
+
updatedAt: currentTime,
|
|
208
|
+
expiresAt: currentTime + PROVIDER_MODELS_CACHE_TTL_MS,
|
|
209
|
+
models,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
memoryCache.set(provider, entry);
|
|
213
|
+
await persistCache();
|
|
214
|
+
return entry;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const loadAndCacheModels = (
|
|
218
|
+
provider: LLMProvider,
|
|
219
|
+
): Promise<ProviderModelsResult> => {
|
|
220
|
+
const request = resolveProvider(provider).models.getSupportedModels()
|
|
221
|
+
.then(async (models) => {
|
|
222
|
+
const entry = await setCacheEntry(provider, models);
|
|
223
|
+
return {
|
|
224
|
+
models,
|
|
225
|
+
cache: toProviderModelsCacheInfo(entry, 'fresh'),
|
|
226
|
+
};
|
|
227
|
+
})
|
|
228
|
+
.finally(() => {
|
|
229
|
+
pendingRequests.delete(provider);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
pendingRequests.set(provider, request);
|
|
233
|
+
return request;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const loadDirectModels = (
|
|
237
|
+
provider: LLMProvider,
|
|
238
|
+
): Promise<ProviderModelsResult> => {
|
|
239
|
+
const request = resolveProvider(provider).models.getSupportedModels()
|
|
240
|
+
.then((models) => {
|
|
241
|
+
const currentTime = now();
|
|
242
|
+
return {
|
|
243
|
+
models,
|
|
244
|
+
cache: {
|
|
245
|
+
updatedAt: new Date(currentTime).toISOString(),
|
|
246
|
+
expiresAt: new Date(currentTime).toISOString(),
|
|
247
|
+
source: 'fresh' as const,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
})
|
|
251
|
+
.finally(() => {
|
|
252
|
+
pendingRequests.delete(provider);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
pendingRequests.set(provider, request);
|
|
256
|
+
return request;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const getProviderModels = async (
|
|
260
|
+
provider: LLMProvider,
|
|
261
|
+
options: ProviderModelsOptions = {},
|
|
262
|
+
): Promise<ProviderModelsResult> => {
|
|
263
|
+
if (UNCACHED_PROVIDERS.has(provider)) {
|
|
264
|
+
const pendingRequest = pendingRequests.get(provider);
|
|
265
|
+
if (pendingRequest) {
|
|
266
|
+
return pendingRequest;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return loadDirectModels(provider);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (options.bypassCache) {
|
|
273
|
+
const pendingRequest = pendingRequests.get(provider);
|
|
274
|
+
if (pendingRequest) {
|
|
275
|
+
return pendingRequest;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return loadAndCacheModels(provider);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const cachedModels = pruneExpiredMemoryEntry(provider, now(), 'memory');
|
|
282
|
+
if (cachedModels) {
|
|
283
|
+
return cachedModels;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const pendingRequest = pendingRequests.get(provider);
|
|
287
|
+
if (pendingRequest) {
|
|
288
|
+
return pendingRequest;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await loadPersistedCache();
|
|
292
|
+
|
|
293
|
+
const persistedModels = pruneExpiredMemoryEntry(provider, now(), 'disk');
|
|
294
|
+
if (persistedModels) {
|
|
295
|
+
return persistedModels;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const postLoadPendingRequest = pendingRequests.get(provider);
|
|
299
|
+
if (postLoadPendingRequest) {
|
|
300
|
+
return postLoadPendingRequest;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return loadAndCacheModels(provider);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const getCurrentActiveModel = async (
|
|
307
|
+
provider: LLMProvider,
|
|
308
|
+
sessionId?: string,
|
|
309
|
+
): Promise<ProviderCurrentActiveModel> => resolveProvider(provider).models.getCurrentActiveModel(sessionId);
|
|
310
|
+
|
|
311
|
+
const changeActiveModel = async (
|
|
312
|
+
provider: LLMProvider,
|
|
313
|
+
input: ProviderChangeActiveModelInput,
|
|
314
|
+
): Promise<ProviderSessionActiveModelChange> => resolveProvider(provider).models.changeActiveModel(input);
|
|
315
|
+
|
|
316
|
+
const getChangedActiveModel = async (
|
|
317
|
+
provider: LLMProvider,
|
|
318
|
+
sessionId: string,
|
|
319
|
+
): Promise<ProviderSessionActiveModelChange> => readProviderSessionActiveModelChange(provider, sessionId, {
|
|
320
|
+
filePath: activeModelChangesPath,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const resolveResumeModel = async (
|
|
324
|
+
provider: LLMProvider,
|
|
325
|
+
sessionId: string | undefined,
|
|
326
|
+
requestedModel?: string | null,
|
|
327
|
+
): Promise<string | undefined> => {
|
|
328
|
+
const normalizedRequestedModel = typeof requestedModel === 'string' ? requestedModel.trim() : '';
|
|
329
|
+
if (!sessionId?.trim()) {
|
|
330
|
+
return normalizedRequestedModel || undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const changedModel = await getChangedActiveModel(provider, sessionId);
|
|
334
|
+
if (changedModel.supported && changedModel.changed && changedModel.model?.trim()) {
|
|
335
|
+
return changedModel.model.trim();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return normalizedRequestedModel || undefined;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const clearCache = (): void => {
|
|
342
|
+
memoryCache.clear();
|
|
343
|
+
pendingRequests.clear();
|
|
344
|
+
persistedCacheLoaded = false;
|
|
345
|
+
persistedCacheLoadPromise = null;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
getProviderModels,
|
|
350
|
+
getCurrentActiveModel,
|
|
351
|
+
getChangedActiveModel,
|
|
352
|
+
changeActiveModel,
|
|
353
|
+
resolveResumeModel,
|
|
354
|
+
clearCache,
|
|
355
|
+
};
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export const providerModelsService = createProviderModelsService();
|
|
@@ -34,6 +34,10 @@ const PROVIDER_WATCH_PATHS: Array<{ provider: LLMProvider; rootPath: string }> =
|
|
|
34
34
|
provider: 'gemini',
|
|
35
35
|
rootPath: path.join(os.homedir(), '.gemini', 'tmp'),
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
provider: 'opencode',
|
|
39
|
+
rootPath: path.join(os.homedir(), '.local', 'share', 'opencode'),
|
|
40
|
+
},
|
|
37
41
|
];
|
|
38
42
|
|
|
39
43
|
const WATCHER_IGNORED_PATTERNS = [
|
|
@@ -67,6 +71,10 @@ let watcherRescheduleAfterRefresh = false;
|
|
|
67
71
|
* Filters watcher events to provider-specific session artifact file types.
|
|
68
72
|
*/
|
|
69
73
|
function isWatcherTargetFile(provider: LLMProvider, filePath: string): boolean {
|
|
74
|
+
if (provider === 'opencode') {
|
|
75
|
+
return path.basename(filePath) === 'opencode.db';
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
if (provider === 'gemini') {
|
|
71
79
|
return filePath.endsWith('.json') || filePath.endsWith('.jsonl');
|
|
72
80
|
}
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
IProvider,
|
|
3
3
|
IProviderAuth,
|
|
4
4
|
IProviderMcp,
|
|
5
|
+
IProviderModels,
|
|
5
6
|
IProviderSessionSynchronizer,
|
|
6
7
|
IProviderSkills,
|
|
7
8
|
IProviderSessions,
|
|
@@ -17,6 +18,7 @@ import type { LLMProvider } from '@/shared/types.js';
|
|
|
17
18
|
*/
|
|
18
19
|
export abstract class AbstractProvider implements IProvider {
|
|
19
20
|
readonly id: LLMProvider;
|
|
21
|
+
abstract readonly models: IProviderModels;
|
|
20
22
|
abstract readonly mcp: IProviderMcp;
|
|
21
23
|
abstract readonly auth: IProviderAuth;
|
|
22
24
|
abstract readonly skills: IProviderSkills;
|
|
@@ -169,6 +169,93 @@ test('providerMcpService handles codex MCP TOML config and capability validation
|
|
|
169
169
|
}
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* This test covers OpenCode MCP support for user/project config files, JSONC-compatible
|
|
174
|
+
* reads, and validation for unsupported scope/transport combinations.
|
|
175
|
+
*/
|
|
176
|
+
test('providerMcpService handles opencode MCP config and capability validation', { concurrency: false }, async () => {
|
|
177
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-opencode-'));
|
|
178
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
179
|
+
await fs.mkdir(workspacePath, { recursive: true });
|
|
180
|
+
await fs.mkdir(path.join(tempRoot, '.config', 'opencode'), { recursive: true });
|
|
181
|
+
await fs.writeFile(
|
|
182
|
+
path.join(tempRoot, '.config', 'opencode', 'opencode.jsonc'),
|
|
183
|
+
`{
|
|
184
|
+
// Existing comments should not block OpenCode MCP reads.
|
|
185
|
+
"mcp": {}
|
|
186
|
+
}\n`,
|
|
187
|
+
'utf8',
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
191
|
+
try {
|
|
192
|
+
await providerMcpService.upsertProviderMcpServer('opencode', {
|
|
193
|
+
name: 'opencode-user-stdio',
|
|
194
|
+
scope: 'user',
|
|
195
|
+
transport: 'stdio',
|
|
196
|
+
command: 'node',
|
|
197
|
+
args: ['server.js'],
|
|
198
|
+
env: { API_KEY: 'x' },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await providerMcpService.upsertProviderMcpServer('opencode', {
|
|
202
|
+
name: 'opencode-project-http',
|
|
203
|
+
scope: 'project',
|
|
204
|
+
transport: 'http',
|
|
205
|
+
url: 'https://opencode.example.com/mcp',
|
|
206
|
+
headers: { Authorization: 'Bearer token' },
|
|
207
|
+
workspacePath,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const userConfig = await readJson(path.join(tempRoot, '.config', 'opencode', 'opencode.jsonc'));
|
|
211
|
+
const userServers = userConfig.mcp as Record<string, unknown>;
|
|
212
|
+
const userStdio = userServers['opencode-user-stdio'] as Record<string, unknown>;
|
|
213
|
+
assert.equal(userStdio.type, 'local');
|
|
214
|
+
assert.deepEqual(userStdio.command, ['node', 'server.js']);
|
|
215
|
+
assert.deepEqual(userStdio.environment, { API_KEY: 'x' });
|
|
216
|
+
|
|
217
|
+
const projectConfig = await readJson(path.join(workspacePath, 'opencode.json'));
|
|
218
|
+
const projectServers = projectConfig.mcp as Record<string, unknown>;
|
|
219
|
+
const projectHttp = projectServers['opencode-project-http'] as Record<string, unknown>;
|
|
220
|
+
assert.equal(projectHttp.type, 'remote');
|
|
221
|
+
assert.equal(projectHttp.url, 'https://opencode.example.com/mcp');
|
|
222
|
+
|
|
223
|
+
const grouped = await providerMcpService.listProviderMcpServers('opencode', { workspacePath });
|
|
224
|
+
assert.ok(grouped.user.some((server) => server.name === 'opencode-user-stdio' && server.transport === 'stdio'));
|
|
225
|
+
assert.ok(grouped.project.some((server) => server.name === 'opencode-project-http' && server.transport === 'http'));
|
|
226
|
+
|
|
227
|
+
await assert.rejects(
|
|
228
|
+
providerMcpService.upsertProviderMcpServer('opencode', {
|
|
229
|
+
name: 'opencode-local',
|
|
230
|
+
scope: 'local',
|
|
231
|
+
transport: 'stdio',
|
|
232
|
+
command: 'node',
|
|
233
|
+
}),
|
|
234
|
+
(error: unknown) =>
|
|
235
|
+
error instanceof AppError &&
|
|
236
|
+
error.code === 'MCP_SCOPE_NOT_SUPPORTED' &&
|
|
237
|
+
error.statusCode === 400,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
await assert.rejects(
|
|
241
|
+
providerMcpService.upsertProviderMcpServer('opencode', {
|
|
242
|
+
name: 'opencode-sse',
|
|
243
|
+
scope: 'project',
|
|
244
|
+
transport: 'sse',
|
|
245
|
+
url: 'https://example.com/sse',
|
|
246
|
+
workspacePath,
|
|
247
|
+
}),
|
|
248
|
+
(error: unknown) =>
|
|
249
|
+
error instanceof AppError &&
|
|
250
|
+
error.code === 'MCP_TRANSPORT_NOT_SUPPORTED' &&
|
|
251
|
+
error.statusCode === 400,
|
|
252
|
+
);
|
|
253
|
+
} finally {
|
|
254
|
+
restoreHomeDir();
|
|
255
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
172
259
|
/**
|
|
173
260
|
* This test covers Gemini/Cursor MCP JSON formats and user/project scope persistence.
|
|
174
261
|
*/
|
|
@@ -254,8 +341,7 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
|
|
|
254
341
|
workspacePath,
|
|
255
342
|
});
|
|
256
343
|
|
|
257
|
-
|
|
258
|
-
assert.equal(globalResult.length, expectCursorGlobal ? 4 : 3);
|
|
344
|
+
assert.equal(globalResult.length, 5);
|
|
259
345
|
assert.ok(globalResult.every((entry) => entry.created === true));
|
|
260
346
|
|
|
261
347
|
const claudeProject = await readJson(path.join(workspacePath, '.mcp.json'));
|
|
@@ -267,10 +353,11 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
|
|
|
267
353
|
const geminiProject = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
|
|
268
354
|
assert.ok((geminiProject.mcpServers as Record<string, unknown>)['global-http']);
|
|
269
355
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
356
|
+
const opencodeProject = await readJson(path.join(workspacePath, 'opencode.json'));
|
|
357
|
+
assert.ok((opencodeProject.mcp as Record<string, unknown>)['global-http']);
|
|
358
|
+
|
|
359
|
+
const cursorProject = await readJson(path.join(workspacePath, '.cursor', 'mcp.json'));
|
|
360
|
+
assert.ok((cursorProject.mcpServers as Record<string, unknown>)['global-http']);
|
|
274
361
|
|
|
275
362
|
await assert.rejects(
|
|
276
363
|
providerMcpService.addMcpServerToAllProviders({
|