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();
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':
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;
11931228 }
11941229}
11951230
1196-async 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 }),
1235+async function loadRemoteEmbeddingModels(source) {
1201- });
1236+ const config = remoteEmbeddingEndpoints[source];
12021237 if (!response.okconfig) {
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([]);
12121239 }
1213-}
12141240
1215-function populateChutesModelSelect(models) {
1241+ const { url, settingsKey, selectId, getBody, filter } = config;
12161242 const selectvalueProperty = $(config.valueProperty || '#vectors_chutes_modelid');
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
1230-async 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);
12381257 }
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);
12421261 populateNanoGPTModelSelect saveSettingsDebounced(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}`);
12721262 }
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- */
1288-function 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;
12991264 }
1300- $('#vectors_electronhub_model').val(settings.electronhub_model);
1301-}
13021265
1303-async function loadOpenRouterModels() {
13041266 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- */
1326-function 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
1341-async function loadSiliconFlowModels() {
1269+ /** @type {RequestInit} */
1342- try {
1270+ const fetchOptions = {
1343- const response = await fetch('/api/openai/siliconflow/models/embedding', {
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- }
1363-}
1364-
1365-function 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-
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}`);
13911286 }
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
1402-function 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();
14151292 }
1416- $('#vectors_workers_ai_model').val(settings.workers_ai_model);
14171293}
14181294
14191295/**