@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,321 +1,228 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { sessionsDb } from '@/modules/database/index.js';
|
|
1
|
+
import { providerModelsDb, sessionsDb } from '@/modules/database/index.js';
|
|
6
2
|
import { providerRegistry } from '@/modules/providers/provider.registry.js';
|
|
7
3
|
import type { IProvider } from '@/shared/interfaces.js';
|
|
8
4
|
import type {
|
|
5
|
+
CustomProviderModelInput,
|
|
6
|
+
CustomProviderModelRecord,
|
|
9
7
|
LLMProvider,
|
|
10
8
|
ProviderCurrentActiveModel,
|
|
11
|
-
|
|
9
|
+
ProviderModelOption,
|
|
12
10
|
ProviderModelsDefinition,
|
|
13
|
-
ProviderModelsResult,
|
|
14
11
|
ProviderSessionModel,
|
|
15
12
|
} from '@/shared/types.js';
|
|
16
|
-
|
|
17
|
-
export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
|
|
18
|
-
const PROVIDER_MODELS_CACHE_VERSION = 2;
|
|
19
|
-
const UNCACHED_PROVIDERS = new Set<LLMProvider>(['claude']);
|
|
13
|
+
import { AppError } from '@/shared/utils.js';
|
|
20
14
|
|
|
21
15
|
/** Session-row access the service needs, narrowed so tests can stub it. */
|
|
22
16
|
type ProviderModelsSessionStore = {
|
|
23
|
-
getSessionById(sessionId: string): { model: string | null } | null;
|
|
17
|
+
getSessionById(sessionId: string): { model: string | null; effort: string | null } | null;
|
|
24
18
|
setSessionModel(sessionId: string, model: string): void;
|
|
19
|
+
setSessionEffort(sessionId: string, effort: string): void;
|
|
25
20
|
};
|
|
26
21
|
|
|
22
|
+
/** SQLite catalog operations used by the Providers service and its unit fakes. */
|
|
23
|
+
type ProviderModelsCatalogStore = Pick<
|
|
24
|
+
typeof providerModelsDb,
|
|
25
|
+
| 'listCustomProviderModels'
|
|
26
|
+
| 'getCustomProviderModel'
|
|
27
|
+
| 'findCustomProviderModelByModelId'
|
|
28
|
+
| 'createCustomProviderModel'
|
|
29
|
+
| 'updateCustomProviderModel'
|
|
30
|
+
| 'deleteCustomProviderModel'
|
|
31
|
+
>;
|
|
32
|
+
|
|
27
33
|
type ProviderModelsServiceDependencies = {
|
|
28
34
|
resolveProvider?: (provider: LLMProvider) => Pick<IProvider, 'models'>;
|
|
29
|
-
|
|
35
|
+
catalog?: ProviderModelsCatalogStore;
|
|
30
36
|
sessions?: ProviderModelsSessionStore;
|
|
31
|
-
now?: () => number;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type ProviderModelsOptions = {
|
|
35
|
-
bypassCache?: boolean;
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
const toCustomProviderModelOption = (
|
|
40
|
+
record: CustomProviderModelRecord,
|
|
41
|
+
): ProviderModelOption => ({
|
|
42
|
+
value: record.modelId,
|
|
43
|
+
label: record.model,
|
|
44
|
+
recordId: record.recordId,
|
|
45
|
+
isCustom: true,
|
|
46
|
+
});
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
const mergeProviderModels = (
|
|
49
|
+
predefined: ProviderModelsDefinition,
|
|
50
|
+
custom: CustomProviderModelRecord[],
|
|
51
|
+
): ProviderModelsDefinition => {
|
|
52
|
+
return {
|
|
53
|
+
OPTIONS: [
|
|
54
|
+
...predefined.OPTIONS.map((option) => ({ ...option, isCustom: false })),
|
|
55
|
+
...custom.map(toCustomProviderModelOption),
|
|
56
|
+
],
|
|
57
|
+
DEFAULT: predefined.DEFAULT,
|
|
58
|
+
};
|
|
47
59
|
};
|
|
48
60
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'provider-models-cache.json',
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const toProviderModelsCacheInfo = (
|
|
56
|
-
entry: ProviderModelsCacheEntry,
|
|
57
|
-
source: ProviderModelsCacheInfo['source'],
|
|
58
|
-
): ProviderModelsCacheInfo => ({
|
|
59
|
-
updatedAt: new Date(entry.updatedAt).toISOString(),
|
|
60
|
-
expiresAt: new Date(entry.expiresAt).toISOString(),
|
|
61
|
-
source,
|
|
61
|
+
const normalizeCustomModelInput = (input: CustomProviderModelInput): CustomProviderModelInput => ({
|
|
62
|
+
id: input.id.trim(),
|
|
63
|
+
model: input.model.trim(),
|
|
62
64
|
});
|
|
63
65
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
&&
|
|
69
|
-
&&
|
|
70
|
-
&& typeof (value as ProviderModelsDefinition['OPTIONS'][number]).label === 'string'
|
|
71
|
-
&& (
|
|
72
|
-
typeof (value as ProviderModelsDefinition['OPTIONS'][number]).description === 'undefined'
|
|
73
|
-
|| typeof (value as ProviderModelsDefinition['OPTIONS'][number]).description === 'string'
|
|
74
|
-
)
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const isProviderModelsDefinition = (value: unknown): value is ProviderModelsDefinition => (
|
|
78
|
-
Boolean(value)
|
|
79
|
-
&& typeof value === 'object'
|
|
80
|
-
&& Array.isArray((value as ProviderModelsDefinition).OPTIONS)
|
|
81
|
-
&& (value as ProviderModelsDefinition).OPTIONS.every(isProviderModelOption)
|
|
82
|
-
&& typeof (value as ProviderModelsDefinition).DEFAULT === 'string'
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const isProviderModelsCacheEntry = (value: unknown): value is ProviderModelsCacheEntry => (
|
|
86
|
-
Boolean(value)
|
|
87
|
-
&& typeof value === 'object'
|
|
88
|
-
&& typeof (value as ProviderModelsCacheEntry).updatedAt === 'number'
|
|
89
|
-
&& typeof (value as ProviderModelsCacheEntry).expiresAt === 'number'
|
|
90
|
-
&& isProviderModelsDefinition((value as ProviderModelsCacheEntry).models)
|
|
66
|
+
const isUniqueConstraintError = (error: unknown): boolean => (
|
|
67
|
+
error !== null
|
|
68
|
+
&& error !== undefined
|
|
69
|
+
&& typeof error === 'object'
|
|
70
|
+
&& 'code' in error
|
|
71
|
+
&& String(error.code).startsWith('SQLITE_CONSTRAINT')
|
|
91
72
|
);
|
|
92
73
|
|
|
93
|
-
const readProviderModelsCacheFile = async (
|
|
94
|
-
cachePath: string,
|
|
95
|
-
): Promise<ProviderModelsCacheFile | null> => {
|
|
96
|
-
try {
|
|
97
|
-
const raw = await readFile(cachePath, 'utf8');
|
|
98
|
-
const parsed = JSON.parse(raw) as Partial<ProviderModelsCacheFile>;
|
|
99
|
-
if (parsed.version !== PROVIDER_MODELS_CACHE_VERSION || !parsed.entries || typeof parsed.entries !== 'object') {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const entries = Object.fromEntries(
|
|
104
|
-
Object.entries(parsed.entries).filter((entry): entry is [string, ProviderModelsCacheEntry] =>
|
|
105
|
-
isProviderModelsCacheEntry(entry[1]),
|
|
106
|
-
),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
version: PROVIDER_MODELS_CACHE_VERSION,
|
|
111
|
-
entries,
|
|
112
|
-
};
|
|
113
|
-
} catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const writeProviderModelsCacheFile = async (
|
|
119
|
-
cachePath: string,
|
|
120
|
-
entries: Map<LLMProvider, ProviderModelsCacheEntry>,
|
|
121
|
-
now: number,
|
|
122
|
-
): Promise<void> => {
|
|
123
|
-
const serializableEntries = Object.fromEntries(
|
|
124
|
-
[...entries.entries()].filter(([, entry]) => entry.expiresAt > now),
|
|
125
|
-
);
|
|
126
|
-
const payload: ProviderModelsCacheFile = {
|
|
127
|
-
version: PROVIDER_MODELS_CACHE_VERSION,
|
|
128
|
-
entries: serializableEntries,
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
132
|
-
await writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
133
|
-
};
|
|
134
|
-
|
|
135
74
|
/**
|
|
136
|
-
*
|
|
75
|
+
* Creates the provider model application service used by Providers routes,
|
|
76
|
+
* Commands, and provider runtimes.
|
|
137
77
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
78
|
+
* Curated adapter definitions stay source-controlled and are merged at read
|
|
79
|
+
* time with custom SQLite rows. This deliberately has no predefined-model
|
|
80
|
+
* persistence, memory cache, disk cache, TTL, or provider-native discovery.
|
|
81
|
+
* Tests inject a small custom-model store through the same boundary.
|
|
141
82
|
*/
|
|
142
83
|
export const createProviderModelsService = (dependencies: ProviderModelsServiceDependencies = {}) => {
|
|
143
84
|
const resolveProvider = dependencies.resolveProvider ?? providerRegistry.resolveProvider;
|
|
144
|
-
const
|
|
85
|
+
const catalog = dependencies.catalog ?? providerModelsDb;
|
|
145
86
|
const sessions = dependencies.sessions ?? sessionsDb;
|
|
146
|
-
const now = dependencies.now ?? (() => Date.now());
|
|
147
|
-
const memoryCache = new Map<LLMProvider, ProviderModelsCacheEntry>();
|
|
148
|
-
const pendingRequests = new Map<LLMProvider, Promise<ProviderModelsResult>>();
|
|
149
|
-
let persistedCacheLoaded = false;
|
|
150
|
-
let persistedCacheLoadPromise: Promise<void> | null = null;
|
|
151
87
|
|
|
152
|
-
const
|
|
88
|
+
const getProviderModels = async (provider: LLMProvider): Promise<ProviderModelsDefinition> => {
|
|
89
|
+
const predefined = await resolveProvider(provider).models.getSupportedModels();
|
|
90
|
+
return mergeProviderModels(predefined, catalog.listCustomProviderModels(provider));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const getCurrentActiveModel = async (
|
|
153
94
|
provider: LLMProvider,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
): ProviderModelsResult | null => {
|
|
157
|
-
const cachedEntry = memoryCache.get(provider);
|
|
158
|
-
if (!cachedEntry) {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
95
|
+
sessionId?: string,
|
|
96
|
+
): Promise<ProviderCurrentActiveModel> => resolveProvider(provider).models.getCurrentActiveModel(sessionId);
|
|
161
97
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
98
|
+
const readCustomModel = (
|
|
99
|
+
provider: LLMProvider,
|
|
100
|
+
recordId: number,
|
|
101
|
+
): CustomProviderModelRecord => {
|
|
102
|
+
const existing = catalog.getCustomProviderModel(provider, recordId);
|
|
103
|
+
if (!existing) {
|
|
104
|
+
throw new AppError('Model not found.', {
|
|
105
|
+
code: 'MODEL_NOT_FOUND',
|
|
106
|
+
statusCode: 404,
|
|
107
|
+
});
|
|
167
108
|
}
|
|
168
109
|
|
|
169
|
-
|
|
170
|
-
return null;
|
|
110
|
+
return existing;
|
|
171
111
|
};
|
|
172
112
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
113
|
+
const assertModelIdAvailable = (
|
|
114
|
+
provider: LLMProvider,
|
|
115
|
+
predefined: ProviderModelsDefinition,
|
|
116
|
+
modelId: string,
|
|
117
|
+
currentRecordId?: number,
|
|
118
|
+
): void => {
|
|
119
|
+
if (predefined.OPTIONS.some((option) => option.value === modelId)) {
|
|
120
|
+
throw new AppError(`A ${provider} model with this ID already exists.`, {
|
|
121
|
+
code: 'MODEL_ID_ALREADY_EXISTS',
|
|
122
|
+
statusCode: 409,
|
|
123
|
+
});
|
|
176
124
|
}
|
|
177
125
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
for (const [provider, entry] of Object.entries(cacheFile?.entries ?? {})) {
|
|
184
|
-
if (entry.expiresAt > currentTime) {
|
|
185
|
-
memoryCache.set(provider as LLMProvider, entry);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
persistedCacheLoaded = true;
|
|
190
|
-
})().finally(() => {
|
|
191
|
-
persistedCacheLoadPromise = null;
|
|
126
|
+
const duplicate = catalog.findCustomProviderModelByModelId(provider, modelId);
|
|
127
|
+
if (duplicate && duplicate.recordId !== currentRecordId) {
|
|
128
|
+
throw new AppError(`A ${provider} model with this ID already exists.`, {
|
|
129
|
+
code: 'MODEL_ID_ALREADY_EXISTS',
|
|
130
|
+
statusCode: 409,
|
|
192
131
|
});
|
|
193
132
|
}
|
|
194
|
-
|
|
195
|
-
await persistedCacheLoadPromise;
|
|
196
133
|
};
|
|
197
134
|
|
|
198
|
-
const
|
|
135
|
+
const createCustomModel = async (
|
|
136
|
+
provider: LLMProvider,
|
|
137
|
+
input: CustomProviderModelInput,
|
|
138
|
+
): Promise<{ model: ProviderModelOption; models: ProviderModelsDefinition }> => {
|
|
139
|
+
const predefined = await resolveProvider(provider).models.getSupportedModels();
|
|
140
|
+
const normalized = normalizeCustomModelInput(input);
|
|
141
|
+
assertModelIdAvailable(provider, predefined, normalized.id);
|
|
142
|
+
|
|
199
143
|
try {
|
|
200
|
-
|
|
144
|
+
const created = catalog.createCustomProviderModel(provider, normalized);
|
|
145
|
+
return {
|
|
146
|
+
model: toCustomProviderModelOption(created),
|
|
147
|
+
models: mergeProviderModels(predefined, catalog.listCustomProviderModels(provider)),
|
|
148
|
+
};
|
|
201
149
|
} catch (error) {
|
|
202
|
-
|
|
150
|
+
if (isUniqueConstraintError(error)) {
|
|
151
|
+
throw new AppError(`A ${provider} model with this ID already exists.`, {
|
|
152
|
+
code: 'MODEL_ID_ALREADY_EXISTS',
|
|
153
|
+
statusCode: 409,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
203
157
|
}
|
|
204
158
|
};
|
|
205
159
|
|
|
206
|
-
const
|
|
207
|
-
provider: LLMProvider,
|
|
208
|
-
models: ProviderModelsDefinition,
|
|
209
|
-
): Promise<ProviderModelsCacheEntry> => {
|
|
210
|
-
const currentTime = now();
|
|
211
|
-
const entry: ProviderModelsCacheEntry = {
|
|
212
|
-
updatedAt: currentTime,
|
|
213
|
-
expiresAt: currentTime + PROVIDER_MODELS_CACHE_TTL_MS,
|
|
214
|
-
models,
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
memoryCache.set(provider, entry);
|
|
218
|
-
await persistCache();
|
|
219
|
-
return entry;
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const loadAndCacheModels = (
|
|
160
|
+
const updateCustomModel = async (
|
|
223
161
|
provider: LLMProvider,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
};
|
|
232
|
-
})
|
|
233
|
-
.finally(() => {
|
|
234
|
-
pendingRequests.delete(provider);
|
|
235
|
-
});
|
|
162
|
+
recordId: number,
|
|
163
|
+
input: CustomProviderModelInput,
|
|
164
|
+
): Promise<{ model: ProviderModelOption; models: ProviderModelsDefinition }> => {
|
|
165
|
+
const predefined = await resolveProvider(provider).models.getSupportedModels();
|
|
166
|
+
readCustomModel(provider, recordId);
|
|
167
|
+
const normalized = normalizeCustomModelInput(input);
|
|
168
|
+
assertModelIdAvailable(provider, predefined, normalized.id, recordId);
|
|
236
169
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const request = resolveProvider(provider).models.getSupportedModels()
|
|
245
|
-
.then((models) => {
|
|
246
|
-
const currentTime = now();
|
|
247
|
-
return {
|
|
248
|
-
models,
|
|
249
|
-
cache: {
|
|
250
|
-
updatedAt: new Date(currentTime).toISOString(),
|
|
251
|
-
expiresAt: new Date(currentTime).toISOString(),
|
|
252
|
-
source: 'fresh' as const,
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
})
|
|
256
|
-
.finally(() => {
|
|
257
|
-
pendingRequests.delete(provider);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
pendingRequests.set(provider, request);
|
|
261
|
-
return request;
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const getProviderModels = async (
|
|
265
|
-
provider: LLMProvider,
|
|
266
|
-
options: ProviderModelsOptions = {},
|
|
267
|
-
): Promise<ProviderModelsResult> => {
|
|
268
|
-
if (UNCACHED_PROVIDERS.has(provider)) {
|
|
269
|
-
const pendingRequest = pendingRequests.get(provider);
|
|
270
|
-
if (pendingRequest) {
|
|
271
|
-
return pendingRequest;
|
|
170
|
+
try {
|
|
171
|
+
const updated = catalog.updateCustomProviderModel(provider, recordId, normalized);
|
|
172
|
+
if (!updated) {
|
|
173
|
+
throw new AppError('Model not found.', {
|
|
174
|
+
code: 'MODEL_NOT_FOUND',
|
|
175
|
+
statusCode: 404,
|
|
176
|
+
});
|
|
272
177
|
}
|
|
273
178
|
|
|
274
|
-
return
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
|
|
179
|
+
return {
|
|
180
|
+
model: toCustomProviderModelOption(updated),
|
|
181
|
+
models: mergeProviderModels(predefined, catalog.listCustomProviderModels(provider)),
|
|
182
|
+
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (isUniqueConstraintError(error)) {
|
|
185
|
+
throw new AppError(`A ${provider} model with this ID already exists.`, {
|
|
186
|
+
code: 'MODEL_ID_ALREADY_EXISTS',
|
|
187
|
+
statusCode: 409,
|
|
188
|
+
});
|
|
281
189
|
}
|
|
282
|
-
|
|
283
|
-
return loadAndCacheModels(provider);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const cachedModels = pruneExpiredMemoryEntry(provider, now(), 'memory');
|
|
287
|
-
if (cachedModels) {
|
|
288
|
-
return cachedModels;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const pendingRequest = pendingRequests.get(provider);
|
|
292
|
-
if (pendingRequest) {
|
|
293
|
-
return pendingRequest;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
await loadPersistedCache();
|
|
297
|
-
|
|
298
|
-
const persistedModels = pruneExpiredMemoryEntry(provider, now(), 'disk');
|
|
299
|
-
if (persistedModels) {
|
|
300
|
-
return persistedModels;
|
|
190
|
+
throw error;
|
|
301
191
|
}
|
|
192
|
+
};
|
|
302
193
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
194
|
+
const deleteCustomModel = async (
|
|
195
|
+
provider: LLMProvider,
|
|
196
|
+
recordId: number,
|
|
197
|
+
): Promise<{ model: ProviderModelOption; models: ProviderModelsDefinition }> => {
|
|
198
|
+
const predefined = await resolveProvider(provider).models.getSupportedModels();
|
|
199
|
+
readCustomModel(provider, recordId);
|
|
200
|
+
const removed = catalog.deleteCustomProviderModel(provider, recordId, predefined.DEFAULT);
|
|
201
|
+
if (!removed) {
|
|
202
|
+
throw new AppError('Model not found.', {
|
|
203
|
+
code: 'MODEL_NOT_FOUND',
|
|
204
|
+
statusCode: 404,
|
|
205
|
+
});
|
|
306
206
|
}
|
|
307
207
|
|
|
308
|
-
return
|
|
208
|
+
return {
|
|
209
|
+
model: toCustomProviderModelOption(removed),
|
|
210
|
+
models: mergeProviderModels(predefined, catalog.listCustomProviderModels(provider)),
|
|
211
|
+
};
|
|
309
212
|
};
|
|
310
213
|
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
): Promise<ProviderCurrentActiveModel> => resolveProvider(provider).models.getCurrentActiveModel(sessionId);
|
|
315
|
-
|
|
316
|
-
const readRecordedSessionModel = (sessionId: string): string | null => {
|
|
214
|
+
const readRecordedSessionSelection = (
|
|
215
|
+
sessionId: string,
|
|
216
|
+
): { model: string | null; effort: string | null } | null => {
|
|
317
217
|
const session = sessions.getSessionById(sessionId);
|
|
318
|
-
|
|
218
|
+
if (!session) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
model: session.model?.trim() || null,
|
|
224
|
+
effort: session.effort?.trim() || null,
|
|
225
|
+
};
|
|
319
226
|
};
|
|
320
227
|
|
|
321
228
|
/**
|
|
@@ -338,7 +245,8 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD
|
|
|
338
245
|
return null;
|
|
339
246
|
}
|
|
340
247
|
|
|
341
|
-
|
|
248
|
+
const recordedSelection = readRecordedSessionSelection(normalizedSessionId);
|
|
249
|
+
if (!recordedSelection) {
|
|
342
250
|
return null;
|
|
343
251
|
}
|
|
344
252
|
|
|
@@ -347,6 +255,38 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD
|
|
|
347
255
|
provider,
|
|
348
256
|
sessionId: normalizedSessionId,
|
|
349
257
|
model: normalizedModel,
|
|
258
|
+
effort: recordedSelection.effort,
|
|
259
|
+
source: 'session',
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Records the reasoning effort one session runs with.
|
|
265
|
+
*
|
|
266
|
+
* Like `setSessionModel`, this ignores an id that has not been allocated by
|
|
267
|
+
* the session gateway yet. The websocket send path records it once the row
|
|
268
|
+
* exists, so a pre-session composer choice is not lost.
|
|
269
|
+
*/
|
|
270
|
+
const setSessionEffort = (
|
|
271
|
+
provider: LLMProvider,
|
|
272
|
+
sessionId: string,
|
|
273
|
+
effort: string,
|
|
274
|
+
): { provider: LLMProvider; sessionId: string; effort: string; source: 'session' } | null => {
|
|
275
|
+
const normalizedSessionId = sessionId.trim();
|
|
276
|
+
const normalizedEffort = effort.trim();
|
|
277
|
+
if (!normalizedSessionId || !normalizedEffort) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!readRecordedSessionSelection(normalizedSessionId)) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
sessions.setSessionEffort(normalizedSessionId, normalizedEffort);
|
|
286
|
+
return {
|
|
287
|
+
provider,
|
|
288
|
+
sessionId: normalizedSessionId,
|
|
289
|
+
effort: normalizedEffort,
|
|
350
290
|
source: 'session',
|
|
351
291
|
};
|
|
352
292
|
};
|
|
@@ -355,13 +295,10 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD
|
|
|
355
295
|
* Answers "which model is this session using?" for every display surface.
|
|
356
296
|
*
|
|
357
297
|
* Precedence, highest first:
|
|
358
|
-
* 1. the model recorded on the session row
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* 3. `requestedModel`, the client's current default, for a chat that has no
|
|
363
|
-
* session yet;
|
|
364
|
-
* 4. the provider catalog default.
|
|
298
|
+
* 1. the model recorded on the session row;
|
|
299
|
+
* 2. the provider's own session state for externally-created sessions;
|
|
300
|
+
* 3. `requestedModel`, the client's current default;
|
|
301
|
+
* 4. the source-controlled provider catalog default.
|
|
365
302
|
*/
|
|
366
303
|
const resolveSessionModel = async (
|
|
367
304
|
provider: LLMProvider,
|
|
@@ -373,26 +310,26 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD
|
|
|
373
310
|
: '';
|
|
374
311
|
|
|
375
312
|
if (normalizedSessionId) {
|
|
376
|
-
const
|
|
377
|
-
if (
|
|
313
|
+
const recordedSelection = readRecordedSessionSelection(normalizedSessionId);
|
|
314
|
+
if (recordedSelection?.model) {
|
|
378
315
|
return {
|
|
379
316
|
provider,
|
|
380
317
|
sessionId: normalizedSessionId,
|
|
381
|
-
model:
|
|
318
|
+
model: recordedSelection.model,
|
|
319
|
+
effort: recordedSelection.effort,
|
|
382
320
|
source: 'session',
|
|
383
321
|
};
|
|
384
322
|
}
|
|
385
323
|
|
|
386
|
-
|
|
387
|
-
// state says before falling back to anything client-supplied.
|
|
388
|
-
const catalog = (await getProviderModels(provider)).models;
|
|
324
|
+
const providerCatalog = await getProviderModels(provider);
|
|
389
325
|
const providerModel = await getCurrentActiveModel(provider, normalizedSessionId);
|
|
390
326
|
const resolvedProviderModel = providerModel.model?.trim();
|
|
391
|
-
if (resolvedProviderModel && resolvedProviderModel !==
|
|
327
|
+
if (resolvedProviderModel && resolvedProviderModel !== providerCatalog.DEFAULT) {
|
|
392
328
|
return {
|
|
393
329
|
provider,
|
|
394
330
|
sessionId: normalizedSessionId,
|
|
395
331
|
model: resolvedProviderModel,
|
|
332
|
+
effort: recordedSelection?.effort ?? null,
|
|
396
333
|
source: 'provider',
|
|
397
334
|
};
|
|
398
335
|
}
|
|
@@ -400,7 +337,8 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD
|
|
|
400
337
|
return {
|
|
401
338
|
provider,
|
|
402
339
|
sessionId: normalizedSessionId,
|
|
403
|
-
model: normalizedRequestedModel ||
|
|
340
|
+
model: normalizedRequestedModel || providerCatalog.DEFAULT,
|
|
341
|
+
effort: recordedSelection?.effort ?? null,
|
|
404
342
|
source: normalizedRequestedModel ? 'session' : 'default',
|
|
405
343
|
};
|
|
406
344
|
}
|
|
@@ -410,26 +348,26 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD
|
|
|
410
348
|
provider,
|
|
411
349
|
sessionId: null,
|
|
412
350
|
model: normalizedRequestedModel,
|
|
351
|
+
effort: null,
|
|
413
352
|
source: 'session',
|
|
414
353
|
};
|
|
415
354
|
}
|
|
416
355
|
|
|
417
|
-
const
|
|
356
|
+
const providerCatalog = await getProviderModels(provider);
|
|
418
357
|
return {
|
|
419
358
|
provider,
|
|
420
359
|
sessionId: null,
|
|
421
|
-
model:
|
|
360
|
+
model: providerCatalog.DEFAULT,
|
|
361
|
+
effort: null,
|
|
422
362
|
source: 'default',
|
|
423
363
|
};
|
|
424
364
|
};
|
|
425
365
|
|
|
426
366
|
/**
|
|
427
|
-
* Picks the model one run should use
|
|
367
|
+
* Picks the model one resumed provider run should use.
|
|
428
368
|
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* from `getCurrentActiveModel`, which would silently override the model the
|
|
432
|
-
* user picked in the composer on every single run.
|
|
369
|
+
* Provider-global state is deliberately ignored because it must never
|
|
370
|
+
* override the model explicitly selected in the composer.
|
|
433
371
|
*/
|
|
434
372
|
const resolveResumeModel = async (
|
|
435
373
|
provider: LLMProvider,
|
|
@@ -443,24 +381,21 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD
|
|
|
443
381
|
return normalizedRequestedModel || undefined;
|
|
444
382
|
}
|
|
445
383
|
|
|
446
|
-
const recordedModel =
|
|
384
|
+
const recordedModel = readRecordedSessionSelection(normalizedSessionId)?.model;
|
|
447
385
|
return recordedModel || normalizedRequestedModel || undefined;
|
|
448
386
|
};
|
|
449
387
|
|
|
450
|
-
const clearCache = (): void => {
|
|
451
|
-
memoryCache.clear();
|
|
452
|
-
pendingRequests.clear();
|
|
453
|
-
persistedCacheLoaded = false;
|
|
454
|
-
persistedCacheLoadPromise = null;
|
|
455
|
-
};
|
|
456
|
-
|
|
457
388
|
return {
|
|
458
389
|
getProviderModels,
|
|
390
|
+
createCustomModel,
|
|
391
|
+
updateCustomModel,
|
|
392
|
+
deleteCustomModel,
|
|
459
393
|
setSessionModel,
|
|
394
|
+
setSessionEffort,
|
|
460
395
|
resolveSessionModel,
|
|
461
396
|
resolveResumeModel,
|
|
462
|
-
clearCache,
|
|
463
397
|
};
|
|
464
398
|
};
|
|
465
399
|
|
|
400
|
+
/** Shared Providers service used by routes, Commands, and provider runtimes. */
|
|
466
401
|
export const providerModelsService = createProviderModelsService();
|
|
@@ -29,7 +29,7 @@ const defaultDependencies: ProviderRuntimeServiceDependencies = {
|
|
|
29
29
|
resolveProviderSessionId: (sessionId) => sessionsService.resolveProviderSessionId(sessionId),
|
|
30
30
|
resolveResumeModel: (provider, sessionId, requestedModel) =>
|
|
31
31
|
providerModelsService.resolveResumeModel(provider, sessionId, requestedModel),
|
|
32
|
-
getProviderModels: (provider
|
|
32
|
+
getProviderModels: (provider) => providerModelsService.getProviderModels(provider),
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -50,8 +50,7 @@ export function createProviderRuntimeService(
|
|
|
50
50
|
resolveProviderSessionId: dependencies.resolveProviderSessionId,
|
|
51
51
|
resolveResumeModel: (sessionId, requestedModel) =>
|
|
52
52
|
dependencies.resolveResumeModel(provider.id, sessionId, requestedModel),
|
|
53
|
-
getProviderModels: async () =>
|
|
54
|
-
(await dependencies.getProviderModels(provider.id)).models,
|
|
53
|
+
getProviderModels: async () => dependencies.getProviderModels(provider.id),
|
|
55
54
|
normalizeMessage: (raw, sessionId) => provider.sessions.normalizeMessage(raw, sessionId),
|
|
56
55
|
async isProviderInstalled() {
|
|
57
56
|
try {
|