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>
This commit is contained in:
Copilot
2026-04-09 23:50:32 +03:00
committed by GitHub
parent f3521e7007
commit 64e8c8d964
+99 -223
View File
@@ -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<string, RemoteEmbeddingEndpointConfig>} */
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<any>} */
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<any>} */
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<any>} */
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<any>} */
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<any>} */
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<any>} */
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);
}
/**