From 64e8c8d964c74b72b421ed06f1d5706713edb804 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 23:50:32 +0300 Subject: [PATCH] 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> --- public/scripts/extensions/vectors/index.js | 322 +++++++-------------- 1 file changed, 99 insertions(+), 223 deletions(-) diff --git a/public/scripts/extensions/vectors/index.js b/public/scripts/extensions/vectors/index.js index 5e95da528..0b0fcac70 100644 --- a/public/scripts/extensions/vectors/index.js +++ b/public/scripts/extensions/vectors/index.js @@ -121,6 +121,59 @@ const webllmProvider = new WebLlmVectorProvider(); const cachedSummaries = new Map(); const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp']; +/** + * @typedef {object} RemoteEmbeddingEndpointConfig + * @property {string} url - The API endpoint URL + * @property {string} settingsKey - The key in settings for the selected model + * @property {string} selectId - The ID of the select element (without #) + * @property {string} [valueProperty='id'] - Property name for the option value + * @property {string} [textProperty] - Property name for the option text. Falls back to valueProperty + * @property {() => object} [getBody] - Function returning the request body + * @property {(models: any[]) => any[]} [filter] - Optional post-fetch filter for models + */ + +/** @type {Record} */ +const remoteEmbeddingEndpoints = { + chutes: { + url: '/api/openai/chutes/models/embedding', + settingsKey: 'chutes_model', + selectId: 'vectors_chutes_model', + valueProperty: 'slug', + textProperty: 'name', + }, + nanogpt: { + url: '/api/openai/nanogpt/models/embedding', + settingsKey: 'nanogpt_model', + selectId: 'vectors_nanogpt_model', + textProperty: 'name', + }, + electronhub: { + url: '/api/openai/electronhub/models', + settingsKey: 'electronhub_model', + selectId: 'vectors_electronhub_model', + textProperty: 'name', + filter: models => models.filter(m => Array.isArray(m?.endpoints) && m.endpoints.includes('/v1/embeddings')), + }, + openrouter: { + url: '/api/openrouter/models/embedding', + settingsKey: 'openrouter_model', + selectId: 'vectors_openrouter_model', + textProperty: 'name', + }, + siliconflow: { + url: '/api/openai/siliconflow/models/embedding', + settingsKey: 'siliconflow_model', + selectId: 'vectors_siliconflow_model', + getBody: () => ({ siliconflow_endpoint: oai_settings.siliconflow_endpoint }), + }, + workers_ai: { + url: '/api/openai/workers-ai/models/embedding', + settingsKey: 'workers_ai_model', + selectId: 'vectors_workers_ai_model', + getBody: () => ({ workers_ai_account_id: oai_settings.workers_ai_account_id }), + }, +}; + /** * Gets the Collection ID for a file embedded in the chat. * @param {string} fileUrl URL of the file @@ -1168,252 +1221,75 @@ function toggleSettings() { $('#siliconflow_vectorsModel').toggle(settings.source === 'siliconflow'); $('#workers_ai_vectorsModel').toggle(settings.source === 'workers_ai'); $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source)); - switch (settings.source) { - case 'webllm': - loadWebLlmModels(); - break; - case 'electronhub': - loadElectronHubModels(); - break; - case 'openrouter': - loadOpenRouterModels(); - break; - case 'chutes': - loadChutesModels(); - break; - case 'nanogpt': - loadNanoGPTModels(); - break; - case 'siliconflow': - loadSiliconFlowModels(); - break; - case 'workers_ai': - loadWorkersAIModels(); - break; - } -} - -async function loadChutesModels() { - try { - const response = await fetch('/api/openai/chutes/models/embedding', { - method: 'POST', - headers: getRequestHeaders({ omitContentType: true }), - }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - /** @type {Array} */ - const data = await response.json(); - const models = Array.isArray(data) ? data : []; - populateChutesModelSelect(models); - } catch (err) { - console.warn('Chutes models fetch failed', err); - populateChutesModelSelect([]); - } -} - -function populateChutesModelSelect(models) { - const select = $('#vectors_chutes_model'); - select.empty(); - for (const m of models) { - const option = document.createElement('option'); - option.value = m.slug; - option.text = m.name; - select.append(option); - } - if (!settings.chutes_model && models.length) { - settings.chutes_model = models[0].slug; - } - $('#vectors_chutes_model').val(settings.chutes_model); -} - -async function loadNanoGPTModels() { - try { - const response = await fetch('/api/openai/nanogpt/models/embedding', { - method: 'POST', - headers: getRequestHeaders({ omitContentType: true }), - }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - /** @type {Array} */ - const data = await response.json(); - const models = Array.isArray(data) ? data : []; - populateNanoGPTModelSelect(models); - } catch (err) { - console.warn('NanoGPT models fetch failed', err); - populateNanoGPTModelSelect([]); - } -} - -function populateNanoGPTModelSelect(models) { - const select = $('#vectors_nanogpt_model'); - select.empty(); - for (const m of models) { - const option = document.createElement('option'); - option.value = m.id; - option.text = m.name || m.id; - select.append(option); - } - if (!settings.nanogpt_model && models.length) { - settings.nanogpt_model = models[0].id; - } - $('#vectors_nanogpt_model').val(settings.nanogpt_model); -} - -async function loadElectronHubModels() { - try { - const response = await fetch('/api/openai/electronhub/models', { - method: 'POST', - headers: getRequestHeaders({ omitContentType: true }), - }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - /** @type {Array} */ - const data = await response.json(); - // filter by embeddings endpoint - const models = Array.isArray(data) ? data.filter(m => Array.isArray(m?.endpoints) && m.endpoints.includes('/v1/embeddings')) : []; - populateElectronHubModelSelect(models); - } catch (err) { - console.warn('Electron Hub models fetch failed', err); - populateElectronHubModelSelect([]); + if (settings.source === 'webllm') { + loadWebLlmModels(); + } else if (settings.source in remoteEmbeddingEndpoints) { + loadRemoteEmbeddingModels(settings.source); } } /** - * Populates the Electron Hub model select element. - * @param {{ id: string, name: string }[]} models Electron Hub models + * Loads models from a remote embedding endpoint and populates the corresponding select element. + * @param {string} source - The source key matching a remoteEmbeddingEndpoints entry */ -function populateElectronHubModelSelect(models) { - const select = $('#vectors_electronhub_model'); - select.empty(); - for (const m of models) { - const option = document.createElement('option'); - option.value = m.id; - option.text = m.name || m.id; - select.append(option); +async function loadRemoteEmbeddingModels(source) { + const config = remoteEmbeddingEndpoints[source]; + if (!config) { + return; } - if (!settings.electronhub_model && models.length) { - settings.electronhub_model = models[0].id; - } - $('#vectors_electronhub_model').val(settings.electronhub_model); -} -async function loadOpenRouterModels() { - try { - const response = await fetch('/api/openrouter/models/embedding', { - method: 'POST', - headers: getRequestHeaders({ omitContentType: true }), - }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); + const { url, settingsKey, selectId, getBody, filter } = config; + const valueProperty = config.valueProperty || 'id'; + const textProperty = config.textProperty; + + /** + * Populates the select element with the given models. + * @param {any[]} models - Array of model objects + */ + function populateSelect(models) { + const select = $(`#${selectId}`); + select.empty(); + for (const m of models) { + const option = document.createElement('option'); + option.value = m[valueProperty]; + option.text = textProperty ? (m[textProperty] || m[valueProperty]) : m[valueProperty]; + select.append(option); } - /** @type {Array} */ - const data = await response.json(); - const models = Array.isArray(data) ? data : []; - populateOpenRouterModelSelect(models); - } catch (err) { - console.warn('OpenRouter models fetch failed', err); - populateOpenRouterModelSelect([]); + if (!settings[settingsKey] && models.length) { + settings[settingsKey] = models[0][valueProperty]; + Object.assign(extension_settings.vectors, settings); + saveSettingsDebounced(); + } + select.val(settings[settingsKey]); } -} -/** - * Populates the OpenRouter model select element. - * @param {{ id: string, name: string }[]} models OpenRouter models - */ -function populateOpenRouterModelSelect(models) { - const select = $('#vectors_openrouter_model'); - select.empty(); - for (const m of models) { - const option = document.createElement('option'); - option.value = m.id; - option.text = m.name || m.id; - select.append(option); - } - if (!settings.openrouter_model && models.length) { - settings.openrouter_model = models[0].id; - } - $('#vectors_openrouter_model').val(settings.openrouter_model); -} - -async function loadSiliconFlowModels() { try { - const response = await fetch('/api/openai/siliconflow/models/embedding', { + const body = typeof getBody === 'function' ? getBody() : {}; + + /** @type {RequestInit} */ + const fetchOptions = { method: 'POST', headers: getRequestHeaders(), - body: JSON.stringify({ - siliconflow_endpoint: oai_settings.siliconflow_endpoint, - }), - }); + body: JSON.stringify(body || {}), + }; + const response = await fetch(url, fetchOptions); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } /** @type {Array} */ const data = await response.json(); - const models = Array.isArray(data) ? data : []; - populateSiliconFlowModelSelect(models); - } catch (err) { - console.warn('SiliconFlow models fetch failed', err); - populateSiliconFlowModelSelect([]); - } -} - -function populateSiliconFlowModelSelect(models) { - const select = $('#vectors_siliconflow_model'); - select.empty(); - for (const m of models) { - const option = document.createElement('option'); - option.value = m.id; - option.text = m.id; - select.append(option); - } - if (!settings.siliconflow_model && models.length) { - settings.siliconflow_model = models[0].id; - } - $('#vectors_siliconflow_model').val(settings.siliconflow_model); -} - -async function loadWorkersAIModels() { - try { - const response = await fetch('/api/openai/workers-ai/models/embedding', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify({ - workers_ai_account_id: oai_settings.workers_ai_account_id, - }), - }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); + let models = Array.isArray(data) ? data : []; + if (filter) { + models = filter(models); } - /** @type {Array} */ - const data = await response.json(); - const models = Array.isArray(data) ? data : []; - populateWorkersAIModelSelect(models); - } catch (err) { - console.warn('Workers AI models fetch failed', err); - populateWorkersAIModelSelect([]); - } -} -function populateWorkersAIModelSelect(models) { - const select = $('#vectors_workers_ai_model'); - select.empty(); - for (const m of models) { - const option = document.createElement('option'); - option.value = m.id; - option.text = m.id; - select.append(option); + populateSelect(models); + } catch (err) { + console.warn(`${source} models fetch failed`, err); + populateSelect([]); } - if (!settings.workers_ai_model && models.length) { - settings.workers_ai_model = models[0].id; - Object.assign(extension_settings.vectors, settings); - saveSettingsDebounced(); - } - $('#vectors_workers_ai_model').val(settings.workers_ai_model); } /**