coding-tool-x 3.7.7 → 3.7.9
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/web/assets/{Analytics-B6FzPvdJ.js → Analytics-D14GGF2y.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DXVnqR5c.js → ConfigTemplates-C0TeUSZg.js} +1 -1
- package/dist/web/assets/{Home-FHJB7hYI.js → Home-CUh_BsGm.js} +1 -1
- package/dist/web/assets/{PluginManager-D8RToURX.js → PluginManager-DPZvPrT4.js} +1 -1
- package/dist/web/assets/{ProjectList-Cdz4jnOg.js → ProjectList-DUaiCvsF.js} +1 -1
- package/dist/web/assets/{SessionList-XYydFlQg.js → SessionList-XdHNM61o.js} +1 -1
- package/dist/web/assets/{SkillManager-Bu9p7mMb.js → SkillManager-I4LFlsVN.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Bx0Gj3Mk.js → WorkspaceManager-zMI7Lhzf.js} +1 -1
- package/dist/web/assets/index-C5nLTbOk.css +1 -0
- package/dist/web/assets/index-CCP7sRMP.js +2 -0
- package/dist/web/assets/{naive-ui-CVyvdmg1.js → naive-ui-BALVpMgd.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/config/model-metadata.json +9 -1
- package/src/server/api/omp-channels.js +105 -11
- package/src/server/api/omp-config.js +6 -8
- package/src/server/api/settings.js +93 -9
- package/src/server/services/model-definition-schema.js +570 -0
- package/src/server/services/model-detector.js +106 -26
- package/src/server/services/omp-channels.js +9 -1
- package/src/server/services/omp-config.js +22 -0
- package/src/server/services/omp-settings-manager.js +153 -167
- package/dist/web/assets/index-DxkAvn4g.js +0 -2
- package/dist/web/assets/index-S8wrU2Nf.css +0 -1
package/dist/web/index.html
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<link rel="icon" href="/favicon.ico">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>coding-tool-x · AI 编程工作台</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CCP7sRMP.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/markdown-BNJfEgCA.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-B5RqjEjQ.js">
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/naive-ui-
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/naive-ui-BALVpMgd.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/icons-nGdSKQn2.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/vendors-DQA4Hw9v.js">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C5nLTbOk.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -91,7 +91,15 @@
|
|
|
91
91
|
},
|
|
92
92
|
"gpt-5.5": {
|
|
93
93
|
"limit": { "context": 1050000, "output": 128000 },
|
|
94
|
-
"pricing": { "input": 5, "output": 30, "cacheRead": 0.5 }
|
|
94
|
+
"pricing": { "input": 5, "output": 30, "cacheRead": 0.5 },
|
|
95
|
+
"reasoning": true,
|
|
96
|
+
"thinking": {
|
|
97
|
+
"mode": "effort",
|
|
98
|
+
"efforts": ["low", "medium", "high", "xhigh"],
|
|
99
|
+
"defaultLevel": "medium"
|
|
100
|
+
},
|
|
101
|
+
"input": ["text", "image"],
|
|
102
|
+
"supportsTools": true
|
|
95
103
|
},
|
|
96
104
|
"gpt-5.4": {
|
|
97
105
|
"limit": { "context": 1050000, "output": 128000 },
|
|
@@ -24,12 +24,39 @@ const {
|
|
|
24
24
|
const {
|
|
25
25
|
findAuthProviderForKey,
|
|
26
26
|
getCachedOmpAuthProviderSnapshot,
|
|
27
|
-
getOmpAuthProviderCacheMeta
|
|
28
|
-
warmOmpAuthProviderSnapshot
|
|
27
|
+
getOmpAuthProviderCacheMeta
|
|
29
28
|
} = require('../services/omp-auth-providers');
|
|
29
|
+
const {
|
|
30
|
+
MODEL_SCHEMA_VERSION,
|
|
31
|
+
MODEL_METADATA_MODES,
|
|
32
|
+
getPublicModelFieldSchema,
|
|
33
|
+
normalizeCatalogModelList,
|
|
34
|
+
redactSensitiveFields,
|
|
35
|
+
validateModelDefinitions,
|
|
36
|
+
validateProviderConfig
|
|
37
|
+
} = require('../services/model-definition-schema');
|
|
38
|
+
const { getOmpCatalogModels } = require('../services/omp-settings-manager');
|
|
30
39
|
|
|
31
40
|
const CHANNEL_LIST_AUTH_OPTIONS = { accountCheck: false, includeStatus: false };
|
|
32
41
|
|
|
42
|
+
function validateOmpModelPayload(payload = {}) {
|
|
43
|
+
if (payload.modelMetadataMode !== undefined && !MODEL_METADATA_MODES.includes(payload.modelMetadataMode)) {
|
|
44
|
+
return `modelMetadataMode must be one of: ${MODEL_METADATA_MODES.join(', ')}`;
|
|
45
|
+
}
|
|
46
|
+
if (payload.models !== undefined) {
|
|
47
|
+
const validation = validateModelDefinitions(payload.models);
|
|
48
|
+
if (!validation.valid) return validation.error;
|
|
49
|
+
}
|
|
50
|
+
if (payload.modelBindings !== undefined && !Array.isArray(payload.modelBindings)) {
|
|
51
|
+
return 'modelBindings must be an array';
|
|
52
|
+
}
|
|
53
|
+
if (payload.providerConfig !== undefined) {
|
|
54
|
+
const validation = validateProviderConfig(payload.providerConfig);
|
|
55
|
+
if (!validation.valid) return validation.error;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
function uniqueModels(models = []) {
|
|
34
61
|
const seen = new Set();
|
|
35
62
|
const result = [];
|
|
@@ -77,6 +104,8 @@ async function discoverOmpModels(channel, gatewaySourceType, { forceRefresh = fa
|
|
|
77
104
|
models: preferredModels,
|
|
78
105
|
supported: preferredModels.length > 0,
|
|
79
106
|
cached: false,
|
|
107
|
+
stale: false,
|
|
108
|
+
retryAfter: null,
|
|
80
109
|
fallbackUsed: true,
|
|
81
110
|
fetchedAt: new Date().toISOString(),
|
|
82
111
|
error: preferredModels.length > 0 ? null : 'OAuth 渠道未配置 API Key,无法直接调用远端模型列表接口',
|
|
@@ -91,11 +120,27 @@ async function discoverOmpModels(channel, gatewaySourceType, { forceRefresh = fa
|
|
|
91
120
|
});
|
|
92
121
|
const listedModels = Array.isArray(listResult.models) ? uniqueModels(listResult.models) : [];
|
|
93
122
|
|
|
123
|
+
if (listResult.backoff || listResult.stale) {
|
|
124
|
+
return {
|
|
125
|
+
models: listedModels,
|
|
126
|
+
supported: listedModels.length > 0,
|
|
127
|
+
cached: !!listResult.cached,
|
|
128
|
+
stale: true,
|
|
129
|
+
retryAfter: listResult.retryAfter || null,
|
|
130
|
+
fallbackUsed: false,
|
|
131
|
+
fetchedAt: listResult.lastChecked || new Date().toISOString(),
|
|
132
|
+
error: listResult.error || (listedModels.length > 0 ? null : '模型目录缓存已过期'),
|
|
133
|
+
errorHint: listResult.errorHint || '可点击“刷新可选模型”更新,或手动填写模型名称。'
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
94
137
|
if (listedModels.length > 0) {
|
|
95
138
|
return {
|
|
96
139
|
models: listedModels,
|
|
97
140
|
supported: true,
|
|
98
141
|
cached: !!listResult.cached,
|
|
142
|
+
stale: false,
|
|
143
|
+
retryAfter: null,
|
|
99
144
|
fallbackUsed: false,
|
|
100
145
|
fetchedAt: listResult.lastChecked || new Date().toISOString(),
|
|
101
146
|
error: null,
|
|
@@ -114,6 +159,8 @@ async function discoverOmpModels(channel, gatewaySourceType, { forceRefresh = fa
|
|
|
114
159
|
models: probedModels,
|
|
115
160
|
supported: probedModels.length > 0,
|
|
116
161
|
cached: !!probe.cached || !!listResult.cached,
|
|
162
|
+
stale: false,
|
|
163
|
+
retryAfter: null,
|
|
117
164
|
fallbackUsed: probedModels.length > 0,
|
|
118
165
|
fetchedAt: probe.lastChecked || listResult.lastChecked || new Date().toISOString(),
|
|
119
166
|
error: probedModels.length > 0 ? null : (listResult.error || '无法获取可用模型'),
|
|
@@ -142,17 +189,12 @@ function attachAuthProvider(channel, snapshot) {
|
|
|
142
189
|
|
|
143
190
|
function getAuthSnapshotForChannelList() {
|
|
144
191
|
const snapshot = getCachedOmpAuthProviderSnapshot(CHANNEL_LIST_AUTH_OPTIONS);
|
|
145
|
-
const shouldWarm = !snapshot || snapshot.stale;
|
|
146
|
-
if (shouldWarm) {
|
|
147
|
-
warmOmpAuthProviderSnapshot(CHANNEL_LIST_AUTH_OPTIONS);
|
|
148
|
-
}
|
|
149
192
|
const meta = getOmpAuthProviderCacheMeta(CHANNEL_LIST_AUTH_OPTIONS);
|
|
150
193
|
return {
|
|
151
194
|
snapshot,
|
|
152
195
|
meta: {
|
|
153
196
|
...meta,
|
|
154
197
|
stale: !snapshot || Boolean(snapshot.stale),
|
|
155
|
-
refreshing: shouldWarm || meta.refreshing,
|
|
156
198
|
fallback: !snapshot
|
|
157
199
|
}
|
|
158
200
|
};
|
|
@@ -241,7 +283,8 @@ module.exports = () => {
|
|
|
241
283
|
model,
|
|
242
284
|
speedTestModel,
|
|
243
285
|
allowedModels,
|
|
244
|
-
modelRedirects
|
|
286
|
+
modelRedirects,
|
|
287
|
+
forceRefresh
|
|
245
288
|
} = req.body || {};
|
|
246
289
|
if (!baseUrl) {
|
|
247
290
|
return res.status(400).json({ error: 'baseUrl is required' });
|
|
@@ -259,11 +302,14 @@ module.exports = () => {
|
|
|
259
302
|
modelRedirects: Array.isArray(modelRedirects) ? modelRedirects : []
|
|
260
303
|
};
|
|
261
304
|
const resolvedGatewaySourceType = resolveGatewaySourceType(tempChannel);
|
|
262
|
-
const result = await discoverOmpModels(tempChannel, resolvedGatewaySourceType, { forceRefresh: true });
|
|
305
|
+
const result = await discoverOmpModels(tempChannel, resolvedGatewaySourceType, { forceRefresh: forceRefresh === true });
|
|
263
306
|
res.json({
|
|
264
307
|
models: result.models,
|
|
265
308
|
supported: result.supported,
|
|
266
309
|
fallbackUsed: result.fallbackUsed,
|
|
310
|
+
cached: result.cached,
|
|
311
|
+
stale: result.stale,
|
|
312
|
+
retryAfter: result.retryAfter,
|
|
267
313
|
error: result.error,
|
|
268
314
|
errorHint: result.errorHint
|
|
269
315
|
});
|
|
@@ -291,6 +337,8 @@ module.exports = () => {
|
|
|
291
337
|
models: result.models,
|
|
292
338
|
supported: result.supported,
|
|
293
339
|
cached: result.cached,
|
|
340
|
+
stale: result.stale,
|
|
341
|
+
retryAfter: result.retryAfter,
|
|
294
342
|
fallbackUsed: result.fallbackUsed,
|
|
295
343
|
fetchedAt: result.fetchedAt,
|
|
296
344
|
error: result.error,
|
|
@@ -302,6 +350,35 @@ module.exports = () => {
|
|
|
302
350
|
}
|
|
303
351
|
});
|
|
304
352
|
|
|
353
|
+
router.post('/catalog-metadata', (req, res) => {
|
|
354
|
+
try {
|
|
355
|
+
const providerKey = String(req.body?.providerKey || '').trim();
|
|
356
|
+
if (!providerKey) {
|
|
357
|
+
return res.status(400).json({ error: 'providerKey is required' });
|
|
358
|
+
}
|
|
359
|
+
const rawModels = getOmpCatalogModels(providerKey, {
|
|
360
|
+
forceCatalogRefresh: req.body?.forceRefresh === true,
|
|
361
|
+
catalogTimeout: 5000
|
|
362
|
+
});
|
|
363
|
+
const normalizedCatalog = normalizeCatalogModelList(rawModels);
|
|
364
|
+
const models = normalizedCatalog.models.map(model => redactSensitiveFields(model));
|
|
365
|
+
const warnings = normalizedCatalog.warnings.slice();
|
|
366
|
+
if (models.length === 0) {
|
|
367
|
+
warnings.push('OMP catalog did not return model metadata; existing manual definitions were left unchanged.');
|
|
368
|
+
}
|
|
369
|
+
res.json({
|
|
370
|
+
schemaVersion: MODEL_SCHEMA_VERSION,
|
|
371
|
+
fieldSchema: getPublicModelFieldSchema(),
|
|
372
|
+
providerKey,
|
|
373
|
+
models,
|
|
374
|
+
warnings
|
|
375
|
+
});
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error('[OMP Channels API] Error reading OMP catalog metadata:', error);
|
|
378
|
+
res.status(502).json({ error: 'Failed to read OMP model metadata' });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
305
382
|
router.post('/', (req, res) => {
|
|
306
383
|
try {
|
|
307
384
|
const {
|
|
@@ -324,9 +401,18 @@ module.exports = () => {
|
|
|
324
401
|
presetId,
|
|
325
402
|
websiteUrl,
|
|
326
403
|
balanceToken,
|
|
327
|
-
balanceUserId
|
|
404
|
+
balanceUserId,
|
|
405
|
+
models,
|
|
406
|
+
modelMetadataMode,
|
|
407
|
+
modelBindings,
|
|
408
|
+
providerConfig
|
|
328
409
|
} = req.body || {};
|
|
329
410
|
|
|
411
|
+
const modelPayloadError = validateOmpModelPayload(req.body || {});
|
|
412
|
+
if (modelPayloadError) {
|
|
413
|
+
return res.status(400).json({ error: modelPayloadError });
|
|
414
|
+
}
|
|
415
|
+
|
|
330
416
|
if (!name || !baseUrl) {
|
|
331
417
|
return res.status(400).json({ error: 'Missing required fields: name and baseUrl' });
|
|
332
418
|
}
|
|
@@ -352,7 +438,11 @@ module.exports = () => {
|
|
|
352
438
|
presetId,
|
|
353
439
|
websiteUrl: websiteUrl || '',
|
|
354
440
|
balanceToken: balanceToken || '',
|
|
355
|
-
balanceUserId: balanceUserId || null
|
|
441
|
+
balanceUserId: balanceUserId || null,
|
|
442
|
+
models: Array.isArray(models) ? models : [],
|
|
443
|
+
modelMetadataMode: modelMetadataMode || 'auto',
|
|
444
|
+
modelBindings: Array.isArray(modelBindings) ? modelBindings : [],
|
|
445
|
+
providerConfig: providerConfig || {}
|
|
356
446
|
});
|
|
357
447
|
res.json(channel);
|
|
358
448
|
broadcastSchedulerState('omp', getSchedulerState('omp'));
|
|
@@ -364,6 +454,10 @@ module.exports = () => {
|
|
|
364
454
|
|
|
365
455
|
router.put('/:channelId', (req, res) => {
|
|
366
456
|
try {
|
|
457
|
+
const modelPayloadError = validateOmpModelPayload(req.body || {});
|
|
458
|
+
if (modelPayloadError) {
|
|
459
|
+
return res.status(400).json({ error: modelPayloadError });
|
|
460
|
+
}
|
|
367
461
|
const channel = updateChannel(req.params.channelId, req.body || {});
|
|
368
462
|
res.json(channel);
|
|
369
463
|
broadcastSchedulerState('omp', getSchedulerState('omp'));
|
|
@@ -6,8 +6,7 @@ const {
|
|
|
6
6
|
getOmpPaths,
|
|
7
7
|
getOmpStatus,
|
|
8
8
|
readJsonFile,
|
|
9
|
-
readYamlFile
|
|
10
|
-
readOmpSettings
|
|
9
|
+
readYamlFile
|
|
11
10
|
} = require('../services/omp-config');
|
|
12
11
|
const {
|
|
13
12
|
getOmpAuthProviderSnapshot
|
|
@@ -89,13 +88,12 @@ function buildCapabilities() {
|
|
|
89
88
|
|
|
90
89
|
router.get('/', (req, res) => {
|
|
91
90
|
try {
|
|
92
|
-
const paths = getOmpPaths();
|
|
93
|
-
const settings =
|
|
91
|
+
const paths = getOmpPaths(process.env, { resolveRuntime: false });
|
|
92
|
+
const settings = readYamlFile(paths.settings, readJsonFile(paths.settingsJsonLegacy, {}));
|
|
94
93
|
const resources = {
|
|
95
94
|
config: readYamlFile(paths.config, {}),
|
|
96
95
|
settings: readYamlFile(paths.settings, {}),
|
|
97
96
|
auth: fs.existsSync(paths.auth) ? { exists: true, path: paths.auth } : { exists: false, path: paths.auth },
|
|
98
|
-
authProviders: getOmpAuthProviderSnapshot({ accountCheck: false }),
|
|
99
97
|
models: readYamlFile(paths.modelsYml, {}),
|
|
100
98
|
legacyModels: readJsonFile(paths.modelsJsonLegacy, {}),
|
|
101
99
|
packages: normalizeArray(settings.packages),
|
|
@@ -110,7 +108,7 @@ router.get('/', (req, res) => {
|
|
|
110
108
|
|
|
111
109
|
res.json({
|
|
112
110
|
success: true,
|
|
113
|
-
status: getOmpStatus(),
|
|
111
|
+
status: getOmpStatus(process.env, { resolveRuntime: false }),
|
|
114
112
|
capabilities: buildCapabilities(),
|
|
115
113
|
resources
|
|
116
114
|
});
|
|
@@ -143,8 +141,8 @@ router.get('/auth-providers', (req, res) => {
|
|
|
143
141
|
|
|
144
142
|
router.get('/resources', (req, res) => {
|
|
145
143
|
try {
|
|
146
|
-
const paths = getOmpPaths();
|
|
147
|
-
const settings =
|
|
144
|
+
const paths = getOmpPaths(process.env, { resolveRuntime: false });
|
|
145
|
+
const settings = readYamlFile(paths.settings, readJsonFile(paths.settingsJsonLegacy, {}));
|
|
148
146
|
res.json({
|
|
149
147
|
success: true,
|
|
150
148
|
paths,
|
|
@@ -7,21 +7,68 @@ const {
|
|
|
7
7
|
saveDefaultSpeedTestModels
|
|
8
8
|
} = require('../../config/model-metadata');
|
|
9
9
|
const { loadConfig, saveConfig } = require('../../config/loader');
|
|
10
|
+
const {
|
|
11
|
+
MODEL_SCHEMA_VERSION,
|
|
12
|
+
getPublicModelFieldSchema,
|
|
13
|
+
isPlainObject,
|
|
14
|
+
redactSensitiveFields,
|
|
15
|
+
resolveModelDefinition,
|
|
16
|
+
validateModelDefinitions
|
|
17
|
+
} = require('../services/model-definition-schema');
|
|
18
|
+
|
|
19
|
+
function mergeMetadata(base = {}, override = {}) {
|
|
20
|
+
const result = { ...base };
|
|
21
|
+
for (const [key, value] of Object.entries(override || {})) {
|
|
22
|
+
if (isPlainObject(value) && isPlainObject(result[key])) result[key] = { ...result[key], ...value };
|
|
23
|
+
else if (value !== undefined) result[key] = value;
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeDefinitions(value) {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
return Object.fromEntries(value
|
|
31
|
+
.filter(item => item && typeof item === 'object' && item.id)
|
|
32
|
+
.map(item => [String(item.id), item]));
|
|
33
|
+
}
|
|
34
|
+
return isPlainObject(value) ? value : {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildResolvedModelSettings(definitions, overrides) {
|
|
38
|
+
const ids = new Set([
|
|
39
|
+
...Object.keys(MODEL_METADATA),
|
|
40
|
+
...Object.keys(overrides || {}),
|
|
41
|
+
...Object.keys(definitions || {})
|
|
42
|
+
]);
|
|
43
|
+
const resolvedModels = {};
|
|
44
|
+
const provenance = {};
|
|
45
|
+
const warnings = {};
|
|
46
|
+
for (const id of ids) {
|
|
47
|
+
const stored = definitions[id] || {
|
|
48
|
+
id,
|
|
49
|
+
metadataMode: 'hybrid',
|
|
50
|
+
overrides: overrides[id] || {}
|
|
51
|
+
};
|
|
52
|
+
const resolved = resolveModelDefinition(stored, { builtin: MODEL_METADATA[id] || {} });
|
|
53
|
+
resolvedModels[id] = resolved.spec;
|
|
54
|
+
provenance[id] = resolved.provenance;
|
|
55
|
+
if (resolved.warnings.length > 0) warnings[id] = resolved.warnings;
|
|
56
|
+
}
|
|
57
|
+
return { resolvedModels, provenance, warnings };
|
|
58
|
+
}
|
|
10
59
|
|
|
11
60
|
function handleGetModelSettings(req, res) {
|
|
12
61
|
try {
|
|
13
62
|
const config = loadConfig();
|
|
14
63
|
const overrides = config.modelMetadataOverrides || {};
|
|
64
|
+
const definitions = normalizeDefinitions(config.modelDefinitions);
|
|
15
65
|
const defaultSpeedTestModels = getDefaultSpeedTestModels();
|
|
16
66
|
|
|
17
67
|
// Build merged table: built-in + user overrides
|
|
18
68
|
const merged = {};
|
|
19
69
|
for (const [id, meta] of Object.entries(MODEL_METADATA)) {
|
|
20
70
|
merged[id] = overrides[id]
|
|
21
|
-
?
|
|
22
|
-
limit: { ...meta.limit, ...(overrides[id].limit || {}) },
|
|
23
|
-
pricing: { ...meta.pricing, ...(overrides[id].pricing || {}) }
|
|
24
|
-
}
|
|
71
|
+
? mergeMetadata(meta, overrides[id])
|
|
25
72
|
: meta;
|
|
26
73
|
}
|
|
27
74
|
|
|
@@ -32,9 +79,16 @@ function handleGetModelSettings(req, res) {
|
|
|
32
79
|
}
|
|
33
80
|
}
|
|
34
81
|
|
|
82
|
+
const resolved = buildResolvedModelSettings(definitions, overrides);
|
|
35
83
|
res.json({
|
|
36
|
-
|
|
37
|
-
|
|
84
|
+
schemaVersion: MODEL_SCHEMA_VERSION,
|
|
85
|
+
fieldSchema: getPublicModelFieldSchema(),
|
|
86
|
+
models: redactSensitiveFields(merged),
|
|
87
|
+
overrides: redactSensitiveFields(overrides),
|
|
88
|
+
definitions: redactSensitiveFields(definitions),
|
|
89
|
+
resolvedModels: redactSensitiveFields(resolved.resolvedModels),
|
|
90
|
+
provenance: resolved.provenance,
|
|
91
|
+
warnings: resolved.warnings,
|
|
38
92
|
builtinModelIds: Object.keys(MODEL_METADATA),
|
|
39
93
|
lastUpdated: METADATA_LAST_UPDATED,
|
|
40
94
|
defaultSpeedTestModels
|
|
@@ -52,7 +106,7 @@ router.get('/model-metadata', handleGetModelSettings);
|
|
|
52
106
|
|
|
53
107
|
function handleSaveModelSettings(req, res) {
|
|
54
108
|
try {
|
|
55
|
-
const { overrides, defaultSpeedTestModels } = req.body || {};
|
|
109
|
+
const { overrides, definitions, defaultSpeedTestModels } = req.body || {};
|
|
56
110
|
if (overrides !== undefined && (typeof overrides !== 'object' || overrides === null || Array.isArray(overrides))) {
|
|
57
111
|
return res.status(400).json({ error: 'overrides must be an object' });
|
|
58
112
|
}
|
|
@@ -63,6 +117,9 @@ function handleSaveModelSettings(req, res) {
|
|
|
63
117
|
if (typeof modelId !== 'string' || !modelId.trim()) {
|
|
64
118
|
return res.status(400).json({ error: `Invalid model ID: "${modelId}"` });
|
|
65
119
|
}
|
|
120
|
+
if (!isPlainObject(meta)) {
|
|
121
|
+
return res.status(400).json({ error: `${modelId}: metadata must be an object` });
|
|
122
|
+
}
|
|
66
123
|
if (meta.limit !== undefined) {
|
|
67
124
|
if (typeof meta.limit !== 'object') {
|
|
68
125
|
return res.status(400).json({ error: `${modelId}: limit must be an object` });
|
|
@@ -87,19 +144,46 @@ function handleSaveModelSettings(req, res) {
|
|
|
87
144
|
}
|
|
88
145
|
}
|
|
89
146
|
|
|
147
|
+
if (definitions !== undefined && !Array.isArray(definitions) && !isPlainObject(definitions)) {
|
|
148
|
+
return res.status(400).json({ error: 'definitions must be an object or array' });
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(definitions)) {
|
|
151
|
+
const validation = validateModelDefinitions(definitions);
|
|
152
|
+
if (!validation.valid) {
|
|
153
|
+
return res.status(400).json({ error: validation.error });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const normalizedDefinitions = definitions === undefined ? undefined : normalizeDefinitions(definitions);
|
|
157
|
+
if (normalizedDefinitions) {
|
|
158
|
+
const definitionList = Object.entries(normalizedDefinitions).map(([id, definition]) => ({
|
|
159
|
+
...(isPlainObject(definition) ? definition : {}),
|
|
160
|
+
id: isPlainObject(definition) && definition.id ? definition.id : id
|
|
161
|
+
}));
|
|
162
|
+
const validation = validateModelDefinitions(definitionList);
|
|
163
|
+
if (!validation.valid) {
|
|
164
|
+
return res.status(400).json({ error: validation.error });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
90
168
|
const config = loadConfig();
|
|
91
169
|
const newConfig = {
|
|
92
170
|
...config,
|
|
93
171
|
modelMetadataOverrides: overrides && typeof overrides === 'object'
|
|
94
172
|
? overrides
|
|
95
|
-
: (config.modelMetadataOverrides || {})
|
|
173
|
+
: (config.modelMetadataOverrides || {}),
|
|
174
|
+
modelDefinitions: normalizedDefinitions === undefined
|
|
175
|
+
? (config.modelDefinitions || {})
|
|
176
|
+
: normalizedDefinitions,
|
|
177
|
+
modelMetadataSchemaVersion: MODEL_SCHEMA_VERSION
|
|
96
178
|
};
|
|
97
179
|
saveConfig(newConfig);
|
|
98
180
|
const persistedDefaultSpeedTestModels = saveDefaultSpeedTestModels(defaultSpeedTestModels);
|
|
99
181
|
|
|
100
182
|
res.json({
|
|
101
183
|
success: true,
|
|
102
|
-
|
|
184
|
+
schemaVersion: MODEL_SCHEMA_VERSION,
|
|
185
|
+
overrides: redactSensitiveFields(newConfig.modelMetadataOverrides),
|
|
186
|
+
definitions: redactSensitiveFields(newConfig.modelDefinitions),
|
|
103
187
|
defaultSpeedTestModels: persistedDefaultSpeedTestModels
|
|
104
188
|
});
|
|
105
189
|
} catch (error) {
|