@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
|
@@ -3,10 +3,17 @@ import express, { type Request, type Response } from 'express';
|
|
|
3
3
|
import { queuedMessagesDb } from '@/modules/database/index.js';
|
|
4
4
|
import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
|
|
5
5
|
import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
|
|
6
|
+
import { providerModelsService } from '@/modules/providers/services/provider-models.service.js';
|
|
6
7
|
import { providerSkillsService } from '@/modules/providers/services/skills.service.js';
|
|
7
8
|
import { sessionConversationsSearchService } from '@/modules/providers/services/session-conversations-search.service.js';
|
|
8
9
|
import { sessionsService } from '@/modules/providers/services/sessions.service.js';
|
|
9
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
LLMProvider,
|
|
12
|
+
McpScope,
|
|
13
|
+
McpTransport,
|
|
14
|
+
ProviderChangeActiveModelInput,
|
|
15
|
+
UpsertProviderMcpServerInput,
|
|
16
|
+
} from '@/shared/types.js';
|
|
10
17
|
import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
|
|
11
18
|
|
|
12
19
|
const router = express.Router();
|
|
@@ -211,7 +218,13 @@ const parseMcpUpsertPayload = (payload: unknown): UpsertProviderMcpServerInput =
|
|
|
211
218
|
|
|
212
219
|
const parseProvider = (value: unknown): LLMProvider => {
|
|
213
220
|
const normalized = normalizeProviderParam(value);
|
|
214
|
-
if (
|
|
221
|
+
if (
|
|
222
|
+
normalized === 'claude'
|
|
223
|
+
|| normalized === 'codex'
|
|
224
|
+
|| normalized === 'cursor'
|
|
225
|
+
|| normalized === 'gemini'
|
|
226
|
+
|| normalized === 'opencode'
|
|
227
|
+
) {
|
|
215
228
|
return normalized;
|
|
216
229
|
}
|
|
217
230
|
|
|
@@ -337,6 +350,29 @@ const parseSessionSearchLimit = (value: unknown): number => {
|
|
|
337
350
|
return Math.max(1, Math.min(parsed, 100));
|
|
338
351
|
};
|
|
339
352
|
|
|
353
|
+
const parseChangeActiveModelPayload = (payload: unknown): ProviderChangeActiveModelInput => {
|
|
354
|
+
if (!payload || typeof payload !== 'object') {
|
|
355
|
+
throw new AppError('Request body must be an object.', {
|
|
356
|
+
code: 'INVALID_REQUEST_BODY',
|
|
357
|
+
statusCode: 400,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const body = payload as Record<string, unknown>;
|
|
362
|
+
const model = readOptionalQueryString(body.model);
|
|
363
|
+
if (!model) {
|
|
364
|
+
throw new AppError('model is required.', {
|
|
365
|
+
code: 'MODEL_REQUIRED',
|
|
366
|
+
statusCode: 400,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
sessionId: '',
|
|
372
|
+
model,
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
|
|
340
376
|
router.get(
|
|
341
377
|
'/:provider/auth/status',
|
|
342
378
|
asyncHandler(async (req: Request, res: Response) => {
|
|
@@ -346,6 +382,30 @@ router.get(
|
|
|
346
382
|
}),
|
|
347
383
|
);
|
|
348
384
|
|
|
385
|
+
router.get(
|
|
386
|
+
'/:provider/models',
|
|
387
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
388
|
+
const provider = parseProvider(req.params.provider);
|
|
389
|
+
const bypassCache = parseOptionalBooleanQuery(req.query.bypassCache, 'bypassCache') ?? false;
|
|
390
|
+
const result = await providerModelsService.getProviderModels(provider, { bypassCache });
|
|
391
|
+
res.json(createApiSuccessResponse({ provider, models: result.models, cache: result.cache }));
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
router.post(
|
|
396
|
+
'/:provider/sessions/:sessionId/active-model',
|
|
397
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
398
|
+
const provider = parseProvider(req.params.provider);
|
|
399
|
+
const sessionId = parseSessionId(req.params.sessionId);
|
|
400
|
+
const payload = parseChangeActiveModelPayload(req.body);
|
|
401
|
+
const result = await providerModelsService.changeActiveModel(provider, {
|
|
402
|
+
...payload,
|
|
403
|
+
sessionId,
|
|
404
|
+
});
|
|
405
|
+
res.json(createApiSuccessResponse(result));
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
|
|
349
409
|
// ----------------- Skills routes -----------------
|
|
350
410
|
router.get(
|
|
351
411
|
'/: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,325 @@
|
|
|
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
|
+
|
|
21
|
+
type ProviderModelsServiceDependencies = {
|
|
22
|
+
resolveProvider?: (provider: LLMProvider) => Pick<IProvider, 'models'>;
|
|
23
|
+
cachePath?: string;
|
|
24
|
+
activeModelChangesPath?: string;
|
|
25
|
+
now?: () => number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ProviderModelsOptions = {
|
|
29
|
+
bypassCache?: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type ProviderModelsCacheEntry = {
|
|
33
|
+
updatedAt: number;
|
|
34
|
+
expiresAt: number;
|
|
35
|
+
models: ProviderModelsDefinition;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type ProviderModelsCacheFile = {
|
|
39
|
+
version: number;
|
|
40
|
+
entries: Record<string, ProviderModelsCacheEntry>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getProviderModelsCachePath = (): string => path.join(
|
|
44
|
+
os.homedir(),
|
|
45
|
+
'.cloudcli',
|
|
46
|
+
'provider-models-cache.json',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const toProviderModelsCacheInfo = (
|
|
50
|
+
entry: ProviderModelsCacheEntry,
|
|
51
|
+
source: ProviderModelsCacheInfo['source'],
|
|
52
|
+
): ProviderModelsCacheInfo => ({
|
|
53
|
+
updatedAt: new Date(entry.updatedAt).toISOString(),
|
|
54
|
+
expiresAt: new Date(entry.expiresAt).toISOString(),
|
|
55
|
+
source,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const isProviderModelOption = (
|
|
59
|
+
value: unknown,
|
|
60
|
+
): value is ProviderModelsDefinition['OPTIONS'][number] => (
|
|
61
|
+
Boolean(value)
|
|
62
|
+
&& typeof value === 'object'
|
|
63
|
+
&& typeof (value as ProviderModelsDefinition['OPTIONS'][number]).value === 'string'
|
|
64
|
+
&& typeof (value as ProviderModelsDefinition['OPTIONS'][number]).label === 'string'
|
|
65
|
+
&& (
|
|
66
|
+
typeof (value as ProviderModelsDefinition['OPTIONS'][number]).description === 'undefined'
|
|
67
|
+
|| typeof (value as ProviderModelsDefinition['OPTIONS'][number]).description === 'string'
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const isProviderModelsDefinition = (value: unknown): value is ProviderModelsDefinition => (
|
|
72
|
+
Boolean(value)
|
|
73
|
+
&& typeof value === 'object'
|
|
74
|
+
&& Array.isArray((value as ProviderModelsDefinition).OPTIONS)
|
|
75
|
+
&& (value as ProviderModelsDefinition).OPTIONS.every(isProviderModelOption)
|
|
76
|
+
&& typeof (value as ProviderModelsDefinition).DEFAULT === 'string'
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const isProviderModelsCacheEntry = (value: unknown): value is ProviderModelsCacheEntry => (
|
|
80
|
+
Boolean(value)
|
|
81
|
+
&& typeof value === 'object'
|
|
82
|
+
&& typeof (value as ProviderModelsCacheEntry).updatedAt === 'number'
|
|
83
|
+
&& typeof (value as ProviderModelsCacheEntry).expiresAt === 'number'
|
|
84
|
+
&& isProviderModelsDefinition((value as ProviderModelsCacheEntry).models)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const readProviderModelsCacheFile = async (
|
|
88
|
+
cachePath: string,
|
|
89
|
+
): Promise<ProviderModelsCacheFile | null> => {
|
|
90
|
+
try {
|
|
91
|
+
const raw = await readFile(cachePath, 'utf8');
|
|
92
|
+
const parsed = JSON.parse(raw) as Partial<ProviderModelsCacheFile>;
|
|
93
|
+
if (parsed.version !== PROVIDER_MODELS_CACHE_VERSION || !parsed.entries || typeof parsed.entries !== 'object') {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const entries = Object.fromEntries(
|
|
98
|
+
Object.entries(parsed.entries).filter((entry): entry is [string, ProviderModelsCacheEntry] =>
|
|
99
|
+
isProviderModelsCacheEntry(entry[1]),
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
version: PROVIDER_MODELS_CACHE_VERSION,
|
|
105
|
+
entries,
|
|
106
|
+
};
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const writeProviderModelsCacheFile = async (
|
|
113
|
+
cachePath: string,
|
|
114
|
+
entries: Map<LLMProvider, ProviderModelsCacheEntry>,
|
|
115
|
+
now: number,
|
|
116
|
+
): Promise<void> => {
|
|
117
|
+
const serializableEntries = Object.fromEntries(
|
|
118
|
+
[...entries.entries()].filter(([, entry]) => entry.expiresAt > now),
|
|
119
|
+
);
|
|
120
|
+
const payload: ProviderModelsCacheFile = {
|
|
121
|
+
version: PROVIDER_MODELS_CACHE_VERSION,
|
|
122
|
+
entries: serializableEntries,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
126
|
+
await writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Provider model lookup service.
|
|
131
|
+
*
|
|
132
|
+
* Routes and other service callers use this layer instead of resolving provider
|
|
133
|
+
* classes directly so the provider-registry dependency stays centralized in one
|
|
134
|
+
* place.
|
|
135
|
+
*/
|
|
136
|
+
export const createProviderModelsService = (dependencies: ProviderModelsServiceDependencies = {}) => {
|
|
137
|
+
const resolveProvider = dependencies.resolveProvider ?? providerRegistry.resolveProvider;
|
|
138
|
+
const cachePath = dependencies.cachePath ?? getProviderModelsCachePath();
|
|
139
|
+
const activeModelChangesPath = dependencies.activeModelChangesPath;
|
|
140
|
+
const now = dependencies.now ?? (() => Date.now());
|
|
141
|
+
const memoryCache = new Map<LLMProvider, ProviderModelsCacheEntry>();
|
|
142
|
+
const pendingRequests = new Map<LLMProvider, Promise<ProviderModelsResult>>();
|
|
143
|
+
let persistedCacheLoaded = false;
|
|
144
|
+
let persistedCacheLoadPromise: Promise<void> | null = null;
|
|
145
|
+
|
|
146
|
+
const pruneExpiredMemoryEntry = (
|
|
147
|
+
provider: LLMProvider,
|
|
148
|
+
currentTime: number,
|
|
149
|
+
source: ProviderModelsCacheInfo['source'],
|
|
150
|
+
): ProviderModelsResult | null => {
|
|
151
|
+
const cachedEntry = memoryCache.get(provider);
|
|
152
|
+
if (!cachedEntry) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (cachedEntry.expiresAt > currentTime) {
|
|
157
|
+
return {
|
|
158
|
+
models: cachedEntry.models,
|
|
159
|
+
cache: toProviderModelsCacheInfo(cachedEntry, source),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
memoryCache.delete(provider);
|
|
164
|
+
return null;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const loadPersistedCache = async (): Promise<void> => {
|
|
168
|
+
if (persistedCacheLoaded) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!persistedCacheLoadPromise) {
|
|
173
|
+
persistedCacheLoadPromise = (async () => {
|
|
174
|
+
const cacheFile = await readProviderModelsCacheFile(cachePath);
|
|
175
|
+
const currentTime = now();
|
|
176
|
+
|
|
177
|
+
for (const [provider, entry] of Object.entries(cacheFile?.entries ?? {})) {
|
|
178
|
+
if (entry.expiresAt > currentTime) {
|
|
179
|
+
memoryCache.set(provider as LLMProvider, entry);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
persistedCacheLoaded = true;
|
|
184
|
+
})().finally(() => {
|
|
185
|
+
persistedCacheLoadPromise = null;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await persistedCacheLoadPromise;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const persistCache = async (): Promise<void> => {
|
|
193
|
+
try {
|
|
194
|
+
await writeProviderModelsCacheFile(cachePath, memoryCache, now());
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.warn('Unable to persist provider models cache:', error);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const setCacheEntry = async (
|
|
201
|
+
provider: LLMProvider,
|
|
202
|
+
models: ProviderModelsDefinition,
|
|
203
|
+
): Promise<ProviderModelsCacheEntry> => {
|
|
204
|
+
const currentTime = now();
|
|
205
|
+
const entry: ProviderModelsCacheEntry = {
|
|
206
|
+
updatedAt: currentTime,
|
|
207
|
+
expiresAt: currentTime + PROVIDER_MODELS_CACHE_TTL_MS,
|
|
208
|
+
models,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
memoryCache.set(provider, entry);
|
|
212
|
+
await persistCache();
|
|
213
|
+
return entry;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const loadAndCacheModels = (
|
|
217
|
+
provider: LLMProvider,
|
|
218
|
+
): Promise<ProviderModelsResult> => {
|
|
219
|
+
const request = resolveProvider(provider).models.getSupportedModels()
|
|
220
|
+
.then(async (models) => {
|
|
221
|
+
const entry = await setCacheEntry(provider, models);
|
|
222
|
+
return {
|
|
223
|
+
models,
|
|
224
|
+
cache: toProviderModelsCacheInfo(entry, 'fresh'),
|
|
225
|
+
};
|
|
226
|
+
})
|
|
227
|
+
.finally(() => {
|
|
228
|
+
pendingRequests.delete(provider);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
pendingRequests.set(provider, request);
|
|
232
|
+
return request;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const getProviderModels = async (
|
|
236
|
+
provider: LLMProvider,
|
|
237
|
+
options: ProviderModelsOptions = {},
|
|
238
|
+
): Promise<ProviderModelsResult> => {
|
|
239
|
+
if (options.bypassCache) {
|
|
240
|
+
const pendingRequest = pendingRequests.get(provider);
|
|
241
|
+
if (pendingRequest) {
|
|
242
|
+
return pendingRequest;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return loadAndCacheModels(provider);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const cachedModels = pruneExpiredMemoryEntry(provider, now(), 'memory');
|
|
249
|
+
if (cachedModels) {
|
|
250
|
+
return cachedModels;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const pendingRequest = pendingRequests.get(provider);
|
|
254
|
+
if (pendingRequest) {
|
|
255
|
+
return pendingRequest;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await loadPersistedCache();
|
|
259
|
+
|
|
260
|
+
const persistedModels = pruneExpiredMemoryEntry(provider, now(), 'disk');
|
|
261
|
+
if (persistedModels) {
|
|
262
|
+
return persistedModels;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const postLoadPendingRequest = pendingRequests.get(provider);
|
|
266
|
+
if (postLoadPendingRequest) {
|
|
267
|
+
return postLoadPendingRequest;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return loadAndCacheModels(provider);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const getCurrentActiveModel = async (
|
|
274
|
+
provider: LLMProvider,
|
|
275
|
+
sessionId?: string,
|
|
276
|
+
): Promise<ProviderCurrentActiveModel> => resolveProvider(provider).models.getCurrentActiveModel(sessionId);
|
|
277
|
+
|
|
278
|
+
const changeActiveModel = async (
|
|
279
|
+
provider: LLMProvider,
|
|
280
|
+
input: ProviderChangeActiveModelInput,
|
|
281
|
+
): Promise<ProviderSessionActiveModelChange> => resolveProvider(provider).models.changeActiveModel(input);
|
|
282
|
+
|
|
283
|
+
const getChangedActiveModel = async (
|
|
284
|
+
provider: LLMProvider,
|
|
285
|
+
sessionId: string,
|
|
286
|
+
): Promise<ProviderSessionActiveModelChange> => readProviderSessionActiveModelChange(provider, sessionId, {
|
|
287
|
+
filePath: activeModelChangesPath,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const resolveResumeModel = async (
|
|
291
|
+
provider: LLMProvider,
|
|
292
|
+
sessionId: string | undefined,
|
|
293
|
+
requestedModel?: string | null,
|
|
294
|
+
): Promise<string | undefined> => {
|
|
295
|
+
const normalizedRequestedModel = typeof requestedModel === 'string' ? requestedModel.trim() : '';
|
|
296
|
+
if (!sessionId?.trim()) {
|
|
297
|
+
return normalizedRequestedModel || undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const changedModel = await getChangedActiveModel(provider, sessionId);
|
|
301
|
+
if (changedModel.supported && changedModel.changed && changedModel.model?.trim()) {
|
|
302
|
+
return changedModel.model.trim();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return normalizedRequestedModel || undefined;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const clearCache = (): void => {
|
|
309
|
+
memoryCache.clear();
|
|
310
|
+
pendingRequests.clear();
|
|
311
|
+
persistedCacheLoaded = false;
|
|
312
|
+
persistedCacheLoadPromise = null;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
getProviderModels,
|
|
317
|
+
getCurrentActiveModel,
|
|
318
|
+
getChangedActiveModel,
|
|
319
|
+
changeActiveModel,
|
|
320
|
+
resolveResumeModel,
|
|
321
|
+
clearCache,
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export const providerModelsService = createProviderModelsService();
|
|
@@ -54,6 +54,10 @@ const PROVIDER_WATCH_PATHS: Array<{ provider: LLMProvider; rootPath: string }> =
|
|
|
54
54
|
provider: 'gemini',
|
|
55
55
|
rootPath: path.join(os.homedir(), '.gemini', 'tmp'),
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
provider: 'opencode',
|
|
59
|
+
rootPath: path.join(os.homedir(), '.local', 'share', 'opencode'),
|
|
60
|
+
},
|
|
57
61
|
];
|
|
58
62
|
|
|
59
63
|
const WATCHER_IGNORED_PATTERNS = [
|
|
@@ -87,6 +91,10 @@ let watcherRescheduleAfterRefresh = false;
|
|
|
87
91
|
* Filters watcher events to provider-specific session artifact file types.
|
|
88
92
|
*/
|
|
89
93
|
function isWatcherTargetFile(provider: LLMProvider, filePath: string): boolean {
|
|
94
|
+
if (provider === 'opencode') {
|
|
95
|
+
return path.basename(filePath) === 'opencode.db';
|
|
96
|
+
}
|
|
97
|
+
|
|
90
98
|
if (provider === 'gemini') {
|
|
91
99
|
return filePath.endsWith('.json') || filePath.endsWith('.jsonl');
|
|
92
100
|
}
|
|
@@ -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({
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildOpenCodeDefinitionFromIds,
|
|
6
|
+
parseOpenCodeModelsStdout,
|
|
7
|
+
} from '@/modules/providers/list/opencode/opencode-models.provider.js';
|
|
8
|
+
|
|
9
|
+
test('OpenCode models provider parses plain CLI output and removes duplicates', () => {
|
|
10
|
+
const ids = parseOpenCodeModelsStdout(`
|
|
11
|
+
opencode/big-pickle
|
|
12
|
+
not a model
|
|
13
|
+
anthropic/claude-opus-4-7-fast
|
|
14
|
+
anthropic/claude-opus-4-7-fast
|
|
15
|
+
openai/gpt-5.5-pro
|
|
16
|
+
`);
|
|
17
|
+
|
|
18
|
+
assert.deepEqual(ids, [
|
|
19
|
+
'opencode/big-pickle',
|
|
20
|
+
'anthropic/claude-opus-4-7-fast',
|
|
21
|
+
'openai/gpt-5.5-pro',
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('OpenCode models provider formats frontend labels from provider-prefixed ids', () => {
|
|
26
|
+
const definition = buildOpenCodeDefinitionFromIds([
|
|
27
|
+
'opencode/deepseek-v4-flash-free',
|
|
28
|
+
'opencode/nemotron-3-super-free',
|
|
29
|
+
'anthropic/claude-3-5-sonnet-20241022',
|
|
30
|
+
'anthropic/claude-opus-4-7-fast',
|
|
31
|
+
'openai/gpt-5.4-mini-fast',
|
|
32
|
+
'openai/gpt-5.5-pro',
|
|
33
|
+
'newprovider/alpha-v12-special-20261231',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
assert.deepEqual(definition.OPTIONS, [
|
|
37
|
+
{
|
|
38
|
+
value: 'opencode/deepseek-v4-flash-free',
|
|
39
|
+
label: 'Deepseek V4 Flash Free',
|
|
40
|
+
description: 'opencode - opencode/deepseek-v4-flash-free',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
value: 'opencode/nemotron-3-super-free',
|
|
44
|
+
label: 'Nemotron 3 Super Free',
|
|
45
|
+
description: 'opencode - opencode/nemotron-3-super-free',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
value: 'anthropic/claude-3-5-sonnet-20241022',
|
|
49
|
+
label: 'Claude 3.5 Sonnet (2024-10-22)',
|
|
50
|
+
description: 'anthropic - anthropic/claude-3-5-sonnet-20241022',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: 'anthropic/claude-opus-4-7-fast',
|
|
54
|
+
label: 'Claude Opus 4.7 Fast',
|
|
55
|
+
description: 'anthropic - anthropic/claude-opus-4-7-fast',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: 'openai/gpt-5.4-mini-fast',
|
|
59
|
+
label: 'GPT-5.4 Mini Fast',
|
|
60
|
+
description: 'openai - openai/gpt-5.4-mini-fast',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
value: 'openai/gpt-5.5-pro',
|
|
64
|
+
label: 'GPT-5.5 Pro',
|
|
65
|
+
description: 'openai - openai/gpt-5.5-pro',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
value: 'newprovider/alpha-v12-special-20261231',
|
|
69
|
+
label: 'Alpha V12 Special (2026-12-31)',
|
|
70
|
+
description: 'newprovider - newprovider/alpha-v12-special-20261231',
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
});
|