feat: [Electron Hub] Support Vector Storage, Better searching for image engine (#4540)

* feat: [Electron Hub] Add Vector Storage, Better searching for image model list

* feat: [Electron Hub] Add quality parameter for Image Engine

* fixed ESLint

* Update public/scripts/extensions/vectors/index.js

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* small tweaks

* Use default getOpenAIVector

* Refactor and clean-up code

* Move endpoint filtering logic to backend

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
Ngo Dinh Gia Bao
2025-10-21 16:38:12 -04:00
committed by GitHub
parent 4add4f0090
commit f0ceba43e9
8 changed files with 181 additions and 6 deletions
@@ -1277,9 +1277,16 @@ async function validateComfyUrl() {
}
async function onModelChange() {
extension_settings.sd.model = $('#sd_model').find(':selected').val();
const selectedModel = $('#sd_model').find(':selected');
extension_settings.sd.model = selectedModel.val();
saveSettingsDebounced();
if (extension_settings.sd.model && extension_settings.sd.source === sources.electronhub) {
const cachedModel = selectedModel.data('model');
const models = cachedModel ? [cachedModel] : await loadElectronHubModels();
ensureElectronHubQualitySelect(models);
}
const cloudSources = [
sources.horde,
sources.novel,
@@ -1726,11 +1733,16 @@ async function loadModels() {
break;
}
if (extension_settings.sd.source === sources.electronhub) {
ensureElectronHubQualitySelect(models);
}
for (const model of models) {
const option = document.createElement('option');
option.innerText = model.text;
option.value = model.value;
option.selected = model.value === extension_settings.sd.model;
$(option).data('model', model);
$('#sd_model').append(option);
}
@@ -1740,6 +1752,49 @@ async function loadModels() {
}
}
/**
* Ensure the Electron Hub quality select is populated based on the selected model.
* @param {any[]} models Array of models
*/
function ensureElectronHubQualitySelect(models) {
try {
const modelId = String(extension_settings.sd.model || '');
if (!modelId) return;
const model = Array.isArray(models) ? models.find(m => String(m?.id) === modelId) : undefined;
const qualities = Array.isArray(model?.qualities) ? model.qualities : undefined;
const $qualityRow = $('#sd_electronhub_quality_row');
const $select = $('#sd_electronhub_quality');
$qualityRow.toggle(!!qualities && qualities.length > 0);
$select.empty();
if (!qualities || qualities.length === 0) {
extension_settings.sd.electronhub_quality = undefined;
saveSettingsDebounced();
return;
}
for (const q of qualities) {
const opt = document.createElement('option');
opt.value = String(q);
opt.textContent = String(q);
opt.selected = String(q) === String(extension_settings.sd.electronhub_quality || '');
$select.append(opt);
}
if (!$select.val()) {
const first = String(qualities[0]);
extension_settings.sd.electronhub_quality = first;
$select.val(first);
saveSettingsDebounced();
}
} catch (e) {
console.error(e);
}
}
async function loadStabilityModels() {
$('#sd_stability_key').toggleClass('success', !!secret_state[SECRET_KEYS.STABILITY]);
@@ -1824,8 +1879,23 @@ async function loadElectronHubModels() {
headers: getRequestHeaders(),
});
function getModelName(model) {
const name = String(model?.name || model?.id || '');
const premium = model?.premium_model ? ' | Premium' : '';
let price = 'Unknown';
if (model?.pricing?.type === 'per_image') {
const coeff = Number(model.pricing.coefficient);
if (!isNaN(coeff)) {
price = `$${coeff}/image`;
}
}
return `${name} | ${price}${premium}`;
}
if (result.ok) {
return await result.json();
/** @type {any[]} */
const data = await result.json();
return Array.isArray(data) ? data.map(m => ({ ...m, text: getModelName(m) })) : [];
}
return [];
@@ -3665,6 +3735,7 @@ async function generateElectronHubImage(prompt, signal) {
model: extension_settings.sd.model,
prompt: prompt,
size: size,
quality: String(extension_settings.sd.electronhub_quality || '').trim() || undefined,
}),
});
@@ -4816,6 +4887,10 @@ jQuery(async () => {
extension_settings.sd.google_enhance = $(this).prop('checked');
saveSettingsDebounced();
});
$('#sd_electronhub_quality').on('change', function () {
extension_settings.sd.electronhub_quality = String($(this).val());
saveSettingsDebounced();
});
if (!CSS.supports('field-sizing', 'content')) {
$('.sd_settings .inline-drawer-toggle').on('click', function () {
@@ -96,6 +96,12 @@
</div>
<div data-sd-source="electronhub">
<i data-i18n="Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.">Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.</i>
<div class="flex-container" id="sd_electronhub_quality_row">
<div class="flex1">
<label for="sd_electronhub_quality" data-i18n="Image Quality">Image Quality</label>
<select id="sd_electronhub_quality"></select>
</div>
</div>
</div>
<div data-sd-source="nanogpt">
<i data-i18n="Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.">Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.</i>
+57 -2
View File
@@ -61,6 +61,7 @@ const settings = {
include_wi: false,
togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval',
openai_model: 'text-embedding-ada-002',
electronhub_model: 'text-embedding-3-small',
cohere_model: 'embed-english-v3.0',
ollama_model: 'mxbai-embed-large',
ollama_keep: false,
@@ -771,6 +772,9 @@ function getVectorsRequestBody(args = {}) {
body.extrasUrl = extension_settings.apiUrl;
body.extrasKey = extension_settings.apiKey;
break;
case 'electronhub':
body.model = extension_settings.vectors.electronhub_model;
break;
case 'togetherai':
body.model = extension_settings.vectors.togetherai_model;
break;
@@ -889,6 +893,7 @@ async function insertVectorItems(collectionId, items) {
*/
function throwIfSourceInvalid() {
if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] ||
settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] ||
settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] ||
@@ -1101,6 +1106,7 @@ function toggleSettings() {
$('#vectors_world_info_settings').toggle(!!settings.enabled_world_info);
$('#together_vectorsModel').toggle(settings.source === 'togetherai');
$('#openai_vectorsModel').toggle(settings.source === 'openai');
$('#electronhub_vectorsModel').toggle(settings.source === 'electronhub');
$('#cohere_vectorsModel').toggle(settings.source === 'cohere');
$('#ollama_vectorsModel').toggle(settings.source === 'ollama');
$('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp');
@@ -1110,11 +1116,55 @@ function toggleSettings() {
$('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp');
$('#google_vectorsModel').toggle(settings.source === 'palm' || settings.source === 'vertexai');
$('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source));
if (settings.source === 'webllm') {
loadWebLlmModels();
switch (settings.source) {
case 'webllm':
loadWebLlmModels();
break;
case 'electronhub':
loadElectronHubModels();
break;
}
}
async function loadElectronHubModels() {
try {
const response = await fetch('/api/openai/electronhub/models', {
method: 'POST',
headers: getRequestHeaders(),
});
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([]);
}
}
/**
* Populates the Electron Hub model select element.
* @param {{ id: string, name: string }[]} models Electron Hub models
*/
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);
}
if (!settings.electronhub_model && models.length) {
settings.electronhub_model = models[0].id;
}
$('#vectors_electronhub_model').val(settings.electronhub_model);
}
/**
* Executes a function with WebLLM error handling.
* @param {function(): Promise<T>} func Function to execute
@@ -1513,6 +1563,11 @@ jQuery(async () => {
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_electronhub_model').val(settings.electronhub_model).on('change', () => {
settings.electronhub_model = String($('#vectors_electronhub_model').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_cohere_model').val(settings.cohere_model).on('change', () => {
settings.cohere_model = String($('#vectors_cohere_model').val());
Object.assign(extension_settings.vectors, settings);
@@ -11,6 +11,7 @@
</label>
<select id="vectors_source" class="text_pole">
<option value="cohere">Cohere</option>
<option value="electronhub">Electron Hub</option>
<option value="extras">Extras (deprecated)</option>
<option value="palm">Google AI Studio</option>
<option value="vertexai">Google Vertex AI</option>
@@ -26,6 +27,15 @@
<option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
</select>
</div>
<div class="flex-container flexFlowColumn" id="electronhub_vectorsModel">
<label for="vectors_electronhub_model" data-i18n="Vectorization Model">
Vectorization Model
</label>
<select id="vectors_electronhub_model" class="text_pole"></select>
<i data-i18n="Hint: Set your Electron Hub API key in API Connections.">
Hint: Set your Electron Hub API key in API Connections.
</i>
</div>
<div class="flex-container flexFlowColumn" id="vector_altEndpointUrl">
<label class="checkbox_label" for="vector_altEndpointUrl_enabled" title="Enable secondary endpoint URL usage, instead of the main one.">
<input id="vector_altEndpointUrl_enabled" type="checkbox" class="checkbox">
+1 -1
View File
@@ -4820,7 +4820,7 @@ function getElectronHubMaxContext(model, isUnlocked) {
return modelInfo.tokens;
}
}
return max_8k;
return max_128k;
}
/**
+15 -1
View File
@@ -983,7 +983,15 @@ electronhub.post('/models', async (request, response) => {
/** @type {any} */
const data = await modelsResponse.json();
const models = data.data.filter(x => x.endpoints.includes('/v1/images/generations')).map(x => ({ value: x.id, text: x.name }));
if (!Array.isArray(data?.data)) {
console.warn('Electron Hub returned invalid data.');
return response.sendStatus(500);
}
const models = data.data
.filter(x => x && Array.isArray(x.endpoints) && x.endpoints.includes('/v1/images/generations'))
.map(x => ({ ...x, value: x.id, text: x.name }));
return response.send(models);
} catch (error) {
console.error(error);
@@ -1010,6 +1018,12 @@ electronhub.post('/generate', async (request, response) => {
bodyParams.size = request.body.size;
}
if (request.body.quality) {
bodyParams.quality = request.body.quality;
}
console.debug('Electron Hub request:', bodyParams);
const result = await fetch('https://api.electronhub.ai/v1/images/generations', {
method: 'POST',
headers: {
+10
View File
@@ -34,6 +34,7 @@ const SOURCES = [
'webllm',
'koboldcpp',
'vertexai',
'electronhub',
];
/**
@@ -53,6 +54,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) {
case 'mistral':
case 'openai':
return getOpenAIVector(text, source, directories, sourceSettings.model);
case 'electronhub':
return getOpenAIVector(text, source, directories, sourceSettings.model);
case 'transformers':
return getTransformersVector(text);
case 'extras':
@@ -102,6 +105,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie
case 'openai':
results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model));
break;
case 'electronhub':
results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model));
break;
case 'transformers':
results.push(...await getTransformersBatchVector(batch));
break;
@@ -156,6 +162,10 @@ function getSourceSettings(source, request) {
return {
model: String(request.body.model),
};
case 'electronhub':
return {
model: String(request.body.model || 'text-embedding-3-small'),
};
case 'cohere':
return {
model: String(request.body.model),
+5
View File
@@ -17,6 +17,11 @@ const SOURCES = {
url: 'api.openai.com',
model: 'text-embedding-ada-002',
},
'electronhub': {
secretKey: SECRET_KEYS.ELECTRONHUB,
url: 'api.electronhub.ai',
model: 'text-embedding-3-small',
},
};
/**