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:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user