Refactor repetitive vectorization model loading into a generic data-driven function (#5425) * refactor: replace 12 individual load/populate functions with generic loadRemoteEmbeddingModels Replaced 6 pairs of load+populate functions (Chutes, NanoGPT, ElectronHub, OpenRouter, SiliconFlow, WorkersAI) with a single data-driven generic function and a configuration map, following the pattern used by the caption extension's processEndpoint helper. Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/29bd42f8-b35b-442f-91fe-bd6c1092436e Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * address review: always include body, use typeof check, remove omitContentType Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/ccfc6ba8-57fd-4411-8e12-106b5e11be86 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>

64e8c8d964c74b72b421ed06f1d5706713edb804

Copilot <198982749+Copilot@users.noreply.github.com>

Signed
1 files changed, +98 -222Ignore whitespace
public/scripts/extensions/vectors/index.js+98 -222
@@ -122,6 +122,59 @@ const cachedSummaries = new Map();
122const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp'];122const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp'];
123123
124/**124/**
125 * @typedef {object} RemoteEmbeddingEndpointConfig
126 * @property {string} url - The API endpoint URL
127 * @property {string} settingsKey - The key in settings for the selected model
128 * @property {string} selectId - The ID of the select element (without #)
129 * @property {string} [valueProperty='id'] - Property name for the option value
130 * @property {string} [textProperty] - Property name for the option text. Falls back to valueProperty
131 * @property {() => object} [getBody] - Function returning the request body
132 * @property {(models: any[]) => any[]} [filter] - Optional post-fetch filter for models
133 */
134
135/** @type {Record<string, RemoteEmbeddingEndpointConfig>} */
136const remoteEmbeddingEndpoints = {
137 chutes: {
138 url: '/api/openai/chutes/models/embedding',
139 settingsKey: 'chutes_model',
140 selectId: 'vectors_chutes_model',
141 valueProperty: 'slug',
142 textProperty: 'name',
143 },
144 nanogpt: {
145 url: '/api/openai/nanogpt/models/embedding',
146 settingsKey: 'nanogpt_model',
147 selectId: 'vectors_nanogpt_model',
148 textProperty: 'name',
149 },
150 electronhub: {
151 url: '/api/openai/electronhub/models',
152 settingsKey: 'electronhub_model',
153 selectId: 'vectors_electronhub_model',
154 textProperty: 'name',
155 filter: models => models.filter(m => Array.isArray(m?.endpoints) && m.endpoints.includes('/v1/embeddings')),
156 },
157 openrouter: {
158 url: '/api/openrouter/models/embedding',
159 settingsKey: 'openrouter_model',
160 selectId: 'vectors_openrouter_model',
161 textProperty: 'name',
162 },
163 siliconflow: {
164 url: '/api/openai/siliconflow/models/embedding',
165 settingsKey: 'siliconflow_model',
166 selectId: 'vectors_siliconflow_model',
167 getBody: () => ({ siliconflow_endpoint: oai_settings.siliconflow_endpoint }),
168 },
169 workers_ai: {
170 url: '/api/openai/workers-ai/models/embedding',
171 settingsKey: 'workers_ai_model',
172 selectId: 'vectors_workers_ai_model',
173 getBody: () => ({ workers_ai_account_id: oai_settings.workers_ai_account_id }),
174 },
175};
176
177/**
125 * Gets the Collection ID for a file embedded in the chat.178 * Gets the Collection ID for a file embedded in the chat.
126 * @param {string} fileUrl URL of the file179 * @param {string} fileUrl URL of the file
127 * @returns {string} Collection ID180 * @returns {string} Collection ID
@@ -1168,252 +1221,75 @@ function toggleSettings() {
1168 $('#siliconflow_vectorsModel').toggle(settings.source === 'siliconflow');1221 $('#siliconflow_vectorsModel').toggle(settings.source === 'siliconflow');
1169 $('#workers_ai_vectorsModel').toggle(settings.source === 'workers_ai');1222 $('#workers_ai_vectorsModel').toggle(settings.source === 'workers_ai');
1170 $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source));1223 $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source));
1171 switch (settings.source) {1224 if (settings.source === 'webllm') {
1172 case 'webllm':1225 loadWebLlmModels();
1173 loadWebLlmModels();1226 } else if (settings.source in remoteEmbeddingEndpoints) {
1174 break;1227 loadRemoteEmbeddingModels(settings.source);
1175 case 'electronhub':
1176 loadElectronHubModels();
1177 break;
1178 case 'openrouter':
1179 loadOpenRouterModels();
1180 break;
1181 case 'chutes':
1182 loadChutesModels();
1183 break;
1184 case 'nanogpt':
1185 loadNanoGPTModels();
1186 break;
1187 case 'siliconflow':
1188 loadSiliconFlowModels();
1189 break;
1190 case 'workers_ai':
1191 loadWorkersAIModels();
1192 break;
1193 }1228 }
1194}1229}
11951230
1196async function loadChutesModels() {1231/**
1197 try {1232 * Loads models from a remote embedding endpoint and populates the corresponding select element.
1198 const response = await fetch('/api/openai/chutes/models/embedding', {1233 * @param {string} source - The source key matching a remoteEmbeddingEndpoints entry
1199 method: 'POST',1234 */
1200 headers: getRequestHeaders({ omitContentType: true }),1235async function loadRemoteEmbeddingModels(source) {
1201 });1236 const config = remoteEmbeddingEndpoints[source];
1202 if (!response.ok) {1237 if (!config) {
1203 throw new Error(`HTTP ${response.status}`);1238 return;
1204 }
1205 /** @type {Array<any>} */
1206 const data = await response.json();
1207 const models = Array.isArray(data) ? data : [];
1208 populateChutesModelSelect(models);
1209 } catch (err) {
1210 console.warn('Chutes models fetch failed', err);
1211 populateChutesModelSelect([]);
1212 }1239 }
1213}
12141240
1215function populateChutesModelSelect(models) {1241 const { url, settingsKey, selectId, getBody, filter } = config;
1216 const select = $('#vectors_chutes_model');1242 const valueProperty = config.valueProperty || 'id';
1217 select.empty();1243 const textProperty = config.textProperty;
1218 for (const m of models) {
1219 const option = document.createElement('option');
1220 option.value = m.slug;
1221 option.text = m.name;
1222 select.append(option);
1223 }
1224 if (!settings.chutes_model && models.length) {
1225 settings.chutes_model = models[0].slug;
1226 }
1227 $('#vectors_chutes_model').val(settings.chutes_model);
1228}
12291244
1230async function loadNanoGPTModels() {1245 /**
1231 try {1246 * Populates the select element with the given models.
1232 const response = await fetch('/api/openai/nanogpt/models/embedding', {1247 * @param {any[]} models - Array of model objects
1233 method: 'POST',1248 */
1234 headers: getRequestHeaders({ omitContentType: true }),1249 function populateSelect(models) {
1235 });1250 const select = $(`#${selectId}`);
1236 if (!response.ok) {1251 select.empty();
1237 throw new Error(`HTTP ${response.status}`);1252 for (const m of models) {
1253 const option = document.createElement('option');
1254 option.value = m[valueProperty];
1255 option.text = textProperty ? (m[textProperty] || m[valueProperty]) : m[valueProperty];
1256 select.append(option);
1238 }1257 }
1239 /** @type {Array<any>} */1258 if (!settings[settingsKey] && models.length) {
1240 const data = await response.json();1259 settings[settingsKey] = models[0][valueProperty];
1241 const models = Array.isArray(data) ? data : [];1260 Object.assign(extension_settings.vectors, settings);
1242 populateNanoGPTModelSelect(models);1261 saveSettingsDebounced();
1243 } catch (err) {
1244 console.warn('NanoGPT models fetch failed', err);
1245 populateNanoGPTModelSelect([]);
1246 }
1247}
1248
1249function populateNanoGPTModelSelect(models) {
1250 const select = $('#vectors_nanogpt_model');
1251 select.empty();
1252 for (const m of models) {
1253 const option = document.createElement('option');
1254 option.value = m.id;
1255 option.text = m.name || m.id;
1256 select.append(option);
1257 }
1258 if (!settings.nanogpt_model && models.length) {
1259 settings.nanogpt_model = models[0].id;
1260 }
1261 $('#vectors_nanogpt_model').val(settings.nanogpt_model);
1262}
1263
1264async function loadElectronHubModels() {
1265 try {
1266 const response = await fetch('/api/openai/electronhub/models', {
1267 method: 'POST',
1268 headers: getRequestHeaders({ omitContentType: true }),
1269 });
1270 if (!response.ok) {
1271 throw new Error(`HTTP ${response.status}`);
1272 }1262 }
1273 /** @type {Array<any>} */1263 select.val(settings[settingsKey]);
1274 const data = await response.json();
1275 // filter by embeddings endpoint
1276 const models = Array.isArray(data) ? data.filter(m => Array.isArray(m?.endpoints) && m.endpoints.includes('/v1/embeddings')) : [];
1277 populateElectronHubModelSelect(models);
1278 } catch (err) {
1279 console.warn('Electron Hub models fetch failed', err);
1280 populateElectronHubModelSelect([]);
1281 }
1282}
1283
1284/**
1285 * Populates the Electron Hub model select element.
1286 * @param {{ id: string, name: string }[]} models Electron Hub models
1287 */
1288function populateElectronHubModelSelect(models) {
1289 const select = $('#vectors_electronhub_model');
1290 select.empty();
1291 for (const m of models) {
1292 const option = document.createElement('option');
1293 option.value = m.id;
1294 option.text = m.name || m.id;
1295 select.append(option);
1296 }
1297 if (!settings.electronhub_model && models.length) {
1298 settings.electronhub_model = models[0].id;
1299 }1264 }
1300 $('#vectors_electronhub_model').val(settings.electronhub_model);
1301}
13021265
1303async function loadOpenRouterModels() {
1304 try {1266 try {
1305 const response = await fetch('/api/openrouter/models/embedding', {1267 const body = typeof getBody === 'function' ? getBody() : {};
1306 method: 'POST',
1307 headers: getRequestHeaders({ omitContentType: true }),
1308 });
1309 if (!response.ok) {
1310 throw new Error(`HTTP ${response.status}`);
1311 }
1312 /** @type {Array<any>} */
1313 const data = await response.json();
1314 const models = Array.isArray(data) ? data : [];
1315 populateOpenRouterModelSelect(models);
1316 } catch (err) {
1317 console.warn('OpenRouter models fetch failed', err);
1318 populateOpenRouterModelSelect([]);
1319 }
1320}
1321
1322/**
1323 * Populates the OpenRouter model select element.
1324 * @param {{ id: string, name: string }[]} models OpenRouter models
1325 */
1326function populateOpenRouterModelSelect(models) {
1327 const select = $('#vectors_openrouter_model');
1328 select.empty();
1329 for (const m of models) {
1330 const option = document.createElement('option');
1331 option.value = m.id;
1332 option.text = m.name || m.id;
1333 select.append(option);
1334 }
1335 if (!settings.openrouter_model && models.length) {
1336 settings.openrouter_model = models[0].id;
1337 }
1338 $('#vectors_openrouter_model').val(settings.openrouter_model);
1339}
13401268
1341async function loadSiliconFlowModels() {1269 /** @type {RequestInit} */
1342 try {1270 const fetchOptions = {
1343 const response = await fetch('/api/openai/siliconflow/models/embedding', {
1344 method: 'POST',1271 method: 'POST',
1345 headers: getRequestHeaders(),1272 headers: getRequestHeaders(),
1346 body: JSON.stringify({1273 body: JSON.stringify(body || {}),
1347 siliconflow_endpoint: oai_settings.siliconflow_endpoint,1274 };
1348 }),
1349 });
13501275
1276 const response = await fetch(url, fetchOptions);
1351 if (!response.ok) {1277 if (!response.ok) {
1352 throw new Error(`HTTP ${response.status}`);1278 throw new Error(`HTTP ${response.status}`);
1353 }1279 }
13541280
1355 /** @type {Array<any>} */1281 /** @type {Array<any>} */
1356 const data = await response.json();1282 const data = await response.json();
1357 const models = Array.isArray(data) ? data : [];1283 let models = Array.isArray(data) ? data : [];
1358 populateSiliconFlowModelSelect(models);1284 if (filter) {
1359 } catch (err) {1285 models = filter(models);
1360 console.warn('SiliconFlow models fetch failed', err);
1361 populateSiliconFlowModelSelect([]);
1362 }
1363}
1364
1365function populateSiliconFlowModelSelect(models) {
1366 const select = $('#vectors_siliconflow_model');
1367 select.empty();
1368 for (const m of models) {
1369 const option = document.createElement('option');
1370 option.value = m.id;
1371 option.text = m.id;
1372 select.append(option);
1373 }
1374 if (!settings.siliconflow_model && models.length) {
1375 settings.siliconflow_model = models[0].id;
1376 }
1377 $('#vectors_siliconflow_model').val(settings.siliconflow_model);
1378}
1379
1380async function loadWorkersAIModels() {
1381 try {
1382 const response = await fetch('/api/openai/workers-ai/models/embedding', {
1383 method: 'POST',
1384 headers: getRequestHeaders(),
1385 body: JSON.stringify({
1386 workers_ai_account_id: oai_settings.workers_ai_account_id,
1387 }),
1388 });
1389 if (!response.ok) {
1390 throw new Error(`HTTP ${response.status}`);
1391 }1286 }
1392 /** @type {Array<any>} */
1393 const data = await response.json();
1394 const models = Array.isArray(data) ? data : [];
1395 populateWorkersAIModelSelect(models);
1396 } catch (err) {
1397 console.warn('Workers AI models fetch failed', err);
1398 populateWorkersAIModelSelect([]);
1399 }
1400}
14011287
1402function populateWorkersAIModelSelect(models) {1288 populateSelect(models);
1403 const select = $('#vectors_workers_ai_model');1289 } catch (err) {
1404 select.empty();1290 console.warn(`${source} models fetch failed`, err);
1405 for (const m of models) {1291 populateSelect([]);
1406 const option = document.createElement('option');
1407 option.value = m.id;
1408 option.text = m.id;
1409 select.append(option);
1410 }
1411 if (!settings.workers_ai_model && models.length) {
1412 settings.workers_ai_model = models[0].id;
1413 Object.assign(extension_settings.vectors, settings);
1414 saveSettingsDebounced();
1415 }1292 }
1416 $('#vectors_workers_ai_model').val(settings.workers_ai_model);
1417}1293}
14181294
1419/**1295/**