Add Electron Hub as Chat Completions Provider (#4458)

* fixed merge conflicts

* Supported max tokens + fixed wrong image model mapping

* fixed merge conflicts

* fixed merge conflicts

* updated the logic

* updated the logic

* replaced hard coded reasoning_effort mode list with a dynamic function

* replaced hard coded reasoning_effort model list with a dynamic function

* Fix eslint

* Adjust reasoning effort logic

* Code clean-up

* Add logo

* Add inline image quality

* Fix multimodal models list

* Fix seed not passed

* Add "detail" error parser

---------

Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
Ngo Dinh Gia Bao
2025-09-04 14:25:31 -04:00
committed by GitHub
parent d134abd50e
commit 8687bb99f3
31 changed files with 573 additions and 25 deletions
@@ -82,6 +82,7 @@ const sources = {
pollinations: 'pollinations',
stability: 'stability',
huggingface: 'huggingface',
electronhub: 'electronhub',
nanogpt: 'nanogpt',
bfl: 'bfl',
falai: 'falai',
@@ -1289,6 +1290,7 @@ async function onModelChange() {
sources.pollinations,
sources.stability,
sources.huggingface,
sources.electronhub,
sources.nanogpt,
sources.bfl,
sources.falai,
@@ -1506,6 +1508,9 @@ async function loadSamplers() {
case sources.huggingface:
samplers = ['N/A'];
break;
case sources.electronhub:
samplers = ['N/A'];
break;
case sources.nanogpt:
samplers = ['N/A'];
break;
@@ -1702,6 +1707,9 @@ async function loadModels() {
case sources.huggingface:
models = [{ value: '', text: '<Enter Model ID above>' }];
break;
case sources.electronhub:
models = await loadElectronHubModels();
break;
case sources.nanogpt:
models = await loadNanoGPTModels();
break;
@@ -1806,6 +1814,24 @@ async function loadTogetherAIModels() {
return [];
}
async function loadElectronHubModels() {
if (!secret_state[SECRET_KEYS.ELECTRONHUB]) {
console.debug('Electron Hub API key is not set.');
return [];
}
const result = await fetch('/api/sd/electronhub/models', {
method: 'POST',
headers: getRequestHeaders(),
});
if (result.ok) {
return await result.json();
}
return [];
}
async function loadNanoGPTModels() {
if (!secret_state[SECRET_KEYS.NANOGPT]) {
console.debug('NanoGPT API key is not set.');
@@ -2131,6 +2157,9 @@ async function loadSchedulers() {
case sources.huggingface:
schedulers = ['N/A'];
break;
case sources.electronhub:
schedulers = ['N/A'];
break;
case sources.nanogpt:
schedulers = ['N/A'];
break;
@@ -2228,6 +2257,9 @@ async function loadVaes() {
case sources.huggingface:
vaes = ['N/A'];
break;
case sources.electronhub:
vaes = ['N/A'];
break;
case sources.nanogpt:
vaes = ['N/A'];
break;
@@ -2811,6 +2843,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
case sources.huggingface:
result = await generateHuggingFaceImage(prefixedPrompt, signal);
break;
case sources.electronhub:
result = await generateElectronHubImage(prefixedPrompt, signal);
break;
case sources.nanogpt:
result = await generateNanoGPTImage(prefixedPrompt, negativePrompt, signal);
break;
@@ -3013,6 +3048,56 @@ function getClosestAspectRatio(width, height, source) {
return closestAspectRatio;
}
/**
* Get closest size for Electron Hub
* @param {number} width - The width of the image
* @param {number} height - The height of the image
* @returns {Promise<string>} - The closest size
*/
async function getClosestSize(width, height) {
const response = await fetch('/api/sd/electronhub/sizes', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
model: extension_settings.sd.model,
}),
});
if (!response.ok) {
const text = await response.text();
throw new Error(text);
}
const result = await response.json();
const sizesData = result.sizes;
const closestSize = sizesData.reduce((closest, size) => {
if (!size || typeof size !== 'string') {
return closest;
}
const sizeParts = size.split('x');
if (sizeParts.length !== 2) {
return closest;
}
const sizeWidth = Number(sizeParts[0]);
const sizeHeight = Number(sizeParts[1]);
const targetWidth = Number(width);
const targetHeight = Number(height);
if (isNaN(sizeWidth) || isNaN(sizeHeight) || isNaN(targetWidth) || isNaN(targetHeight)) {
return closest;
}
const sizeArea = sizeWidth * sizeHeight;
const targetArea = targetWidth * targetHeight;
const diff = Math.abs(sizeArea - targetArea);
return diff < closest.diff ? { size, diff } : closest;
}, { size: null, diff: Infinity });
const size = closestSize.size;
return size;
}
/**
* Generates an image using Stability AI.
* @param {string} prompt - The main instruction used to guide the image generation.
@@ -3564,6 +3649,35 @@ async function generateHuggingFaceImage(prompt, signal) {
}
}
/**
* Generates an image using the Electron Hub API.
* @param {string} prompt - The main instruction used to guide the image generation.
* @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
*/
async function generateElectronHubImage(prompt, signal) {
const size = await getClosestSize(extension_settings.sd.width, extension_settings.sd.height);
const result = await fetch('/api/sd/electronhub/generate', {
method: 'POST',
headers: getRequestHeaders(),
signal: signal,
body: JSON.stringify({
model: extension_settings.sd.model,
prompt: prompt,
size: size,
}),
});
if (result.ok) {
const data = await result.json();
return { format: 'jpg', data: data.image };
} else {
const text = await result.text();
throw new Error(text);
}
}
/**
* Generates an image using the NanoGPT API.
* @param {string} prompt - The main instruction used to guide the image generation.
@@ -4014,6 +4128,8 @@ function isValidState() {
return secret_state[SECRET_KEYS.STABILITY];
case sources.huggingface:
return secret_state[SECRET_KEYS.HUGGINGFACE];
case sources.electronhub:
return secret_state[SECRET_KEYS.ELECTRONHUB];
case sources.nanogpt:
return secret_state[SECRET_KEYS.NANOGPT];
case sources.bfl: