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, +89 -213Showing whitespace changes
public/scripts/extensions/vectors/index.js+89 -213
@@ -122,6 +122,59 @@ const cachedSummaries = new Map();
122122const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp'];
123123
124124/**
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>} */
136+const 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+/**
125178 * Gets the Collection ID for a file embedded in the chat.
126179 * @param {string} fileUrl URL of the file
127180 * @returns {string} Collection ID
@@ -1168,252 +1221,75 @@ function toggleSettings() {
11681221 $('#siliconflow_vectorsModel').toggle(settings.source === 'siliconflow');
11691222 $('#workers_ai_vectorsModel').toggle(settings.source === 'workers_ai');
11701223 $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source));
11711224 switchif (settings.source === 'webllm') {
1172- case 'webllm':
11731225 loadWebLlmModels();
1174- break;
1226+ } else if (settings.source in remoteEmbeddingEndpoints) {
1175- case 'electronhub':
1227+ loadRemoteEmbeddingModels(settings.source);
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- }
1194-}
1195-
1196-async function loadChutesModels() {
1197- try {
1198- const response = await fetch('/api/openai/chutes/models/embedding', {
1199- method: 'POST',
1200- headers: getRequestHeaders({ omitContentType: true }),
1201- });
1202- if (!response.ok) {
1203- throw new Error(`HTTP ${response.status}`);
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- }
1213-}
1214-
1215-function populateChutesModelSelect(models) {
1216- const select = $('#vectors_chutes_model');
1217- select.empty();
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-}
1229-
1230-async function loadNanoGPTModels() {
1231- try {
1232- const response = await fetch('/api/openai/nanogpt/models/embedding', {
1233- method: 'POST',
1234- headers: getRequestHeaders({ omitContentType: true }),
1235- });
1236- if (!response.ok) {
1237- throw new Error(`HTTP ${response.status}`);
1238- }
1239- /** @type {Array<any>} */
1240- const data = await response.json();
1241- const models = Array.isArray(data) ? data : [];
1242- populateNanoGPTModelSelect(models);
1243- } catch (err) {
1244- console.warn('NanoGPT models fetch failed', err);
1245- populateNanoGPTModelSelect([]);
1246- }
1247-}
1248-
1249-function 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-
1264-async 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- }
1273- /** @type {Array<any>} */
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([]);
12811228 }
12821229}
12831230
12841231/**
12851232 * PopulatesLoads themodels Electronfrom Huba modelremote embedding endpoint and populates the corresponding select element.
12861233 * @param {{string} id:source string,- name:The stringsource }[]}key modelsmatching Electrona HubremoteEmbeddingEndpoints modelsentry
12871234 */
12881235async function populateElectronHubModelSelectloadRemoteEmbeddingModels(modelssource) {
12891236 const selectconfig = $('#vectors_electronhub_model')remoteEmbeddingEndpoints[source];
1290- select.empty();
1237+ if (!config) {
1291- for (const m of models) {
1238+ return;
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- }
1300- $('#vectors_electronhub_model').val(settings.electronhub_model);
13011239 }
13021240
1303-async function loadOpenRouterModels() {
1241+ const { url, settingsKey, selectId, getBody, filter } = config;
1304- try {
1242+ const valueProperty = config.valueProperty || 'id';
1305- const response = await fetch('/api/openrouter/models/embedding', {
1243+ const textProperty = config.textProperty;
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-}
13211244
13221245 /**
13231246 * Populates the OpenRouter model select element with the given models.
1324- * @param {{ id: string, name: string }[]} models OpenRouter models
1247+ * @param {any[]} models - Array of model objects
13251248 */
13261249 function populateOpenRouterModelSelectpopulateSelect(models) {
13271250 const select = $('`#vectors_openrouter_model'${selectId}`);
13281251 select.empty();
13291252 for (const m of models) {
13301253 const option = document.createElement('option');
13311254 option.value = m.id[valueProperty];
13321255 option.text = textProperty ? (m.name[textProperty] || m.id[valueProperty]) : m[valueProperty];
13331256 select.append(option);
13341257 }
13351258 if (!settings.openrouter_model[settingsKey] && models.length) {
13361259 settings.openrouter_model[settingsKey] = models[0].id[valueProperty];
1260+ Object.assign(extension_settings.vectors, settings);
1261+ saveSettingsDebounced();
13371262 }
1338- $('#vectors_openrouter_model').val(settings.openrouter_model);
1263+ select.val(settings[settingsKey]);
13391264 }
13401265
1341-async function loadSiliconFlowModels() {
13421266 try {
1343- const response = await fetch('/api/openai/siliconflow/models/embedding', {
1267+ const body = typeof getBody === 'function' ? getBody() : {};
1268+
1269+ /** @type {RequestInit} */
1270+ const fetchOptions = {
13441271 method: 'POST',
13451272 headers: getRequestHeaders(),
13461273 body: JSON.stringify(body || {}),
1347- siliconflow_endpoint: oai_settings.siliconflow_endpoint,
1274+ };
1348- }),
1349- });
13501275
1276+ const response = await fetch(url, fetchOptions);
13511277 if (!response.ok) {
13521278 throw new Error(`HTTP ${response.status}`);
13531279 }
13541280
13551281 /** @type {Array<any>} */
13561282 const data = await response.json();
13571283 constlet 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- }
13631286 }
13641287
1365-function populateSiliconFlowModelSelect(models) {
1288+ populateSelect(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-
1380-async 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- }
1392- /** @type {Array<any>} */
1393- const data = await response.json();
1394- const models = Array.isArray(data) ? data : [];
1395- populateWorkersAIModelSelect(models);
13961289 } catch (err) {
13971290 console.warn('Workers AI`${source} models fetch failed'`, err);
13981291 populateWorkersAIModelSelectpopulateSelect([]);
1399- }
1400-}
1401-
1402-function populateWorkersAIModelSelect(models) {
1403- const select = $('#vectors_workers_ai_model');
1404- select.empty();
1405- for (const m of models) {
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();
14151292 }
1416- $('#vectors_workers_ai_model').val(settings.workers_ai_model);
14171293}
14181294
14191295/**