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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -4820,7 +4820,7 @@ function getElectronHubMaxContext(model, isUnlocked) {
|
||||
return modelInfo.tokens;
|
||||
}
|
||||
}
|
||||
return max_8k;
|
||||
return max_128k;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user