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>

8687bb99f3dd7a20555f82173420c70f34398c95

Ngo Dinh Gia Bao <112873281+snowby666@users.noreply.github.com>

Signed
31 files changed, +562 -14Showing whitespace changes
public/img/electronhub.svg+1 -0
@@ -0,0 +1 @@
1+<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="118.66667" height="177.33333" viewBox="0 0 89 133"><path d="M14.5 1.8c-6.3 3-11.3 8.8-13.2 15.1C-.1 21.8-.2 27.6.6 70.6l.9 48.2 3.1 4.4c1.9 2.6 5.3 5.4 8.4 7l5.2 2.8h26.7c14.8 0 28.2-.5 30.2-1 5.4-1.5 11.6-8.6 12.4-14.3 1-6.5-.2-10.4-4.7-15.3-5.1-5.5-8.3-6.4-23.3-6.4C46.2 96 43 94.9 43 90.5c0-4.3 3.3-5.5 15.4-5.5 13 0 18-1.8 22.7-8.3 5.4-7.5 5-14.4-1.3-21.3-5.1-5.5-11-7.4-22.8-7.4-9.2 0-13-1.5-13-5 0-3.9 3.6-5 16.9-5 14.5 0 19.4-1.6 23.7-7.9 5.4-7.9 5.2-15.9-.6-22.5-5.4-6.1-6.7-6.4-37.5-7.1-26.3-.6-28.2-.5-32 1.3z"/></svg>
1 \ No newline at end of file
public/index.html+28 -13
@@ -691,7 +691,7 @@
691691 </span>
692692 </div>
693693 </div>
694694 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
695695 <div class="range-block-title" data-i18n="Temperature">
696696 Temperature
697697 </div>
@@ -704,7 +704,7 @@
704704 </div>
705705 </div>
706706 </div>
707707 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
708708 <div class="range-block-title" data-i18n="Frequency Penalty">
709709 Frequency Penalty
710710 </div>
@@ -717,7 +717,7 @@
717717 </div>
718718 </div>
719719 </div>
720720 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
721721 <div class="range-block-title" data-i18n="Presence Penalty">
722722 Presence Penalty
723723 </div>
@@ -730,7 +730,7 @@
730730 </div>
731731 </div>
732732 </div>
733733 <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub">
734734 <div class="range-block-title" data-i18n="Top K">
735735 Top K
736736 </div>
@@ -743,7 +743,7 @@
743743 </div>
744744 </div>
745745 </div>
746746 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
747747 <div class="range-block-title" data-i18n="Top P">
748748 Top P
749749 </div>
@@ -980,7 +980,7 @@
980980 </div>
981981 </div>
982982 </div>
983983 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai">
984984 <div class="range-block-title justifyLeft" data-i18n="Seed">
985985 Seed
986986 </div>
@@ -1970,7 +1970,7 @@
19701970 </span>
19711971 </div>
19721972 </div>
19731973 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,electronhub,nanogpt">
19741974 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
19751975 <input id="openai_enable_web_search" type="checkbox" />
19761976 <span data-i18n="Enable web search">Enable web search</span>
@@ -1984,7 +1984,7 @@
19841984 </b>
19851985 </div>
19861986 </div>
19871987 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub">
19881988 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
19891989 <input id="openai_function_calling" type="checkbox" />
19901990 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1999,7 +1999,7 @@
19991999 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
20002000 </div>
20012001 </div>
20022002 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub">
20032003 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
20042004 <input id="openai_image_inlining" type="checkbox" />
20052005 <span data-i18n="Send inline images">Send inline images</span>
@@ -2015,7 +2015,7 @@
20152015 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code>
20162016 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
20172017 </div>
20182018 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub">
20192019 <div class="flex-container oneline-dropdown">
20202020 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
20212021 Inline Image Quality
@@ -2077,7 +2077,7 @@
20772077 </span>
20782078 </div>
20792079 </div>
20802080 <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub">
20812081 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
20822082 <input id="openai_show_thoughts" type="checkbox" />
20832083 <span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2091,7 +2091,7 @@
20912091 </span>
20922092 </div>
20932093 </div>
20942094 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub">
20952095 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
20962096 <label for="openai_reasoning_effort">
20972097 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2105,7 +2105,7 @@
21052105 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
21062106 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
21072107 </select>
21082108 <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,perplexity,electronhub" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">
21092109 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
21102110 </div>
21112111 <div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.">
@@ -2807,6 +2807,7 @@
28072807 <!-- Temporarily disabled. -->
28082808 <!-- <option value="cometapi">CometAPI</option> -->
28092809 <option value="deepseek">DeepSeek</option>
2810+ <option value="electronhub">Electron Hub</option>
28102811 <option value="fireworks">Fireworks AI</option>
28112812 <option value="groq">Groq</option>
28122813 <option value="makersuite">Google AI Studio</option>
@@ -3469,6 +3470,20 @@
34693470 <option value="mistral-saba-24b">mistral-saba-24b</option>
34703471 </select>
34713472 </div>
3473+ <div id="electronhub_form" data-source="electronhub">
3474+ <h4 data-i18n="Electron Hub API Key">Electron Hub API Key</h4>
3475+ <div class="flex-container">
3476+ <input id="api_key_electronhub" name="api_key_electronhub" class="text_pole flex1" value="" type="text" autocomplete="off">
3477+ <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_electronhub"></div>
3478+ </div>
3479+ <div data-for="api_key_electronhub" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3480+ For privacy reasons, your API key will be hidden after you click 'Connect'.
3481+ </div>
3482+ <h4 data-i18n="Electron Hub Model">Electron Hub Model</h4>
3483+ <select id="model_electronhub_select">
3484+ <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
3485+ </select>
3486+ </div>
34723487 <div id="nanogpt_form" data-source="nanogpt">
34733488 <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4>
34743489 <div class="flex-container">
public/locales/ar-sa.json+2 -0
@@ -317,6 +317,8 @@
317317 "flag": "وضع علامة",
318318 "API key (optional)": "مفتاح API (اختياري)",
319319 "Server url": "رابط الخادم",
320+ "Electron Hub API Key": "مفتاح API لـ Electron Hub",
321+ "Electron Hub Model": "نموذج Electron Hub",
320322 "Example: http://127.0.0.1:5000": "مثال: http://127.0.0.1:5000",
321323 "Custom model (optional)": "نموذج مخصص (اختياري)",
322324 "vllm-project/vllm": "vllm-project/vllm (وضع غلاف OpenAI API)",
public/locales/de-de.json+2 -0
@@ -317,6 +317,8 @@
317317 "flag": "Flagge",
318318 "API key (optional)": "API-Schlüssel (optional)",
319319 "Server url": "Server-URL",
320+ "Electron Hub API Key": "Electron Hub API-Schlüssel",
321+ "Electron Hub Model": "Electron Hub-Modell",
320322 "Example: http://127.0.0.1:5000": "Beispiel: http://127.0.0.1:5000",
321323 "Custom model (optional)": "Benutzerdefiniertes Modell (optional)",
322324 "vllm-project/vllm": "vllm-project/vllm (OpenAI API-Wrappermodus)",
public/locales/es-es.json+2 -0
@@ -317,6 +317,8 @@
317317 "flag": "bandera",
318318 "API key (optional)": "Clave API (opcional)",
319319 "Server url": "URL del servidor",
320+ "Electron Hub API Key": "Clave API de Electron Hub",
321+ "Electron Hub Model": "Modelo de Electron Hub",
320322 "Example: http://127.0.0.1:5000": "Ejemplo: http://127.0.0.1:5000",
321323 "Custom model (optional)": "Modelo personalizado (opcional)",
322324 "vllm-project/vllm": "vllm-project/vllm (modo contenedor de API OpenAI)",
public/locales/fr-fr.json+2 -0
@@ -1411,6 +1411,8 @@
14111411 "Do not proceed if you do not agree to this!": "Ne continuez pas si vous n'êtes pas d'accord avec cela !",
14121412 "Claude API Key": "Clé API Claude",
14131413 "Allow fallback models": "Autoriser les modèles de secours",
1414+ "Electron Hub API Key": "Clé API Electron Hub",
1415+ "Electron Hub Model": "Modèle Electron Hub",
14141416 "NanoGPT API Key": "Clé API NanoGPT",
14151417 "NanoGPT Model": "Modèle NanoGPT",
14161418 "DeepSeek API Key": "Clé API DeepSeek",
public/locales/it-it.json+2 -0
@@ -317,6 +317,8 @@
317317 "flag": "bandiera",
318318 "API key (optional)": "Chiave API (opzionale)",
319319 "Server url": "URL del server",
320+ "Electron Hub API Key": "Chiave API di Electron Hub",
321+ "Electron Hub Model": "Modello di Electron Hub",
320322 "Example: http://127.0.0.1:5000": "Esempio: http://127.0.0.1:5000",
321323 "Custom model (optional)": "Modello personalizzato (opzionale)",
322324 "vllm-project/vllm": "vllm-project/vllm (modalità wrapper API OpenAI)",
public/locales/ja-jp.json+2 -0
@@ -317,6 +317,8 @@
317317 "flag": "フラグ",
318318 "API key (optional)": "APIキー(オプション)",
319319 "Server url": "サーバーURL",
320+ "Electron Hub API Key": "Electron Hub API キー",
321+ "Electron Hub Model": "Electron Hub モデル",
320322 "Example: http://127.0.0.1:5000": "例: http://127.0.0.1:5000",
321323 "Custom model (optional)": "カスタムモデル(オプション)",
322324 "vllm-project/vllm": "vllm-project/vllm (OpenAI API ラッパーモード)",
public/locales/ko-kr.json+2 -0
@@ -319,6 +319,8 @@
319319 "flag": "깃발",
320320 "API key (optional)": "API 키 (선택 사항)",
321321 "Server url": "서버 URL",
322+ "Electron Hub API Key": "Electron Hub API 키",
323+ "Electron Hub Model": "Electron Hub 모델",
322324 "Example: http://127.0.0.1:5000": "예시: http://127.0.0.1:5000",
323325 "Custom model (optional)": "사용자 정의 모델 (선택 사항)",
324326 "vllm-project/vllm": "vllm-project/vllm(OpenAI API 래퍼 모드)",
public/locales/pt-pt.json+2 -0
@@ -317,6 +317,8 @@
317317 "flag": "bandeira",
318318 "API key (optional)": "Chave da API (opcional)",
319319 "Server url": "URL do servidor",
320+ "Electron Hub API Key": "Chave API Electron Hub",
321+ "Electron Hub Model": "Modelo Electron Hub",
320322 "Example: http://127.0.0.1:5000": "Exemplo: http://127.0.0.1:5000",
321323 "Custom model (optional)": "Modelo personalizado (opcional)",
322324 "vllm-project/vllm": "vllm-project/vllm (modo wrapper da API OpenAI)",
public/locales/ru-ru.json+2 -0
@@ -1994,6 +1994,8 @@
19941994 "Click on the setting name to omit it from the profile.": "Нажмите на название настройки, чтобы исключить её из профиля",
19951995 "Included settings:": "Сохранённые параметры:",
19961996 "Server URL": "Адрес сервера",
1997+ "Electron Hub API Key": "Ключ от API Electron Hub",
1998+ "Electron Hub Model": "Модель Electron Hub",
19971999 "NanoGPT API Key": "Ключ от API NanoGPT",
19982000 "NanoGPT Model": "Модель NanoGPT",
19992001 "Use extension settings": "Использовать настройки из расширения",
public/locales/uk-ua.json+2 -0
@@ -317,6 +317,8 @@
317317 "flag": "прапорцем",
318318 "API key (optional)": "Ключ API (необов'язково)",
319319 "Server url": "URL-адреса сервера",
320+ "Electron Hub API Key": "Ключ API для Electron Hub",
321+ "Electron Hub Model": "Модель Electron Hub",
320322 "Example: http://127.0.0.1:5000": "Приклад: http://127.0.0.1:5000",
321323 "Custom model (optional)": "Власна модель (необов'язково)",
322324 "vllm-project/vllm": "vllm-project/vllm (режим оболонки OpenAI API)",
public/locales/vi-vn.json+2 -0
@@ -317,6 +317,8 @@
317317 "flag": "cờ",
318318 "API key (optional)": "Key API (tùy chọn)",
319319 "Server url": "URL máy chủ",
320+ "Electron Hub API Key": "Key API Electron Hub",
321+ "Electron Hub Model": "Model Electron Hub",
320322 "Example: http://127.0.0.1:5000": "Ví dụ: http://127.0.0.1:5000",
321323 "Custom model (optional)": "Model tùy chỉnh (tùy chọn)",
322324 "vllm-project/vllm": "vllm-project/vllm (Chế độ trình bao bọc API OpenAI)",
public/locales/zh-cn.json+2 -0
@@ -481,6 +481,8 @@
481481 "MistralAI Model": "MistralAI 模型",
482482 "Groq API Key": "Groq API 密钥",
483483 "Groq Model": "Groq 模型",
484+ "Electron Hub API Key": "Electron Hub API 密钥",
485+ "Electron Hub Model": "Electron Hub 模型",
484486 "NanoGPT API Key": "NanoGPT API 密钥",
485487 "NanoGPT Model": "NanoGPT 模型",
486488 "DeepSeek API Key": "DeepSeek API 密钥",
public/locales/zh-tw.json+2 -0
@@ -1791,6 +1791,8 @@
17911791 "Derive context size from backend": "從後端推導上下文大小",
17921792 "Using a proxy that you're not running yourself is a risk to your data privacy.": "使用非自行管理的代理服務可能導致您的資料隱私外洩。",
17931793 "Claude API Key": "Claude API 金鑰",
1794+ "Electron Hub API Key": "Electron Hub API 金鑰",
1795+ "Electron Hub Model": "Electron Hub 模型",
17941796 "NanoGPT API Key": "NanoGPT API 金鑰",
17951797 "NanoGPT Model": "NanoGPT 模型",
17961798 "context_derived": "若可能,根據模型後設資料推導。",
public/script.js+1 -0
@@ -5369,6 +5369,7 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource
53695369 case chat_completion_sources.CUSTOM:
53705370 case chat_completion_sources.COHERE:
53715371 case chat_completion_sources.XAI:
5372+ case chat_completion_sources.ELECTRONHUB:
53725373 default:
53735374 result = tryParse(text);
53745375 break;
public/scripts/RossAscends-mods.js+1 -0
@@ -402,6 +402,7 @@ function RA_autoconnect(PrevApi) {
402402 || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
403403 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
404404 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
405+ || (secret_state[SECRET_KEYS.ELECTRONHUB] && oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB)
405406 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
406407 || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)
407408 || (secret_state[SECRET_KEYS.XAI] && oai_settings.chat_completion_source == chat_completion_sources.XAI)
public/scripts/extensions/caption/index.js+2 -0
@@ -440,6 +440,7 @@ jQuery(async function () {
440440 'aimlapi': SECRET_KEYS.AIMLAPI,
441441 'moonshot': SECRET_KEYS.MOONSHOT,
442442 'nanogpt': SECRET_KEYS.NANOGPT,
443+ 'electronhub': SECRET_KEYS.ELECTRONHUB,
443444 };
444445
445446 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
@@ -547,6 +548,7 @@ jQuery(async function () {
547548 await processEndpoint('aimlapi', '/api/backends/chat-completions/multimodal-models/aimlapi');
548549 await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations');
549550 await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt');
551+ await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub');
550552 }
551553
552554 await addSettings();
public/scripts/extensions/caption/settings.html+1 -0
@@ -21,6 +21,7 @@
2121 <option value="anthropic">Anthropic</option>
2222 <option value="cohere">Cohere</option>
2323 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
24+ <option value="electronhub">Electron Hub</option>
2425 <option value="google">Google AI Studio</option>
2526 <option value="vertexai">Google Vertex AI</option>
2627 <option value="groq">Groq</option>
public/scripts/extensions/shared.js+4 -0
@@ -251,6 +251,10 @@ function throwIfInvalidModel(useReverseProxy) {
251251 if (multimodalApi === 'nanogpt' && !secret_state[SECRET_KEYS.NANOGPT]) {
252252 throw new Error('NanoGPT API key is not set.');
253253 }
254+
255+ if (multimodalApi === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB]) {
256+ throw new Error('Electron Hub API key is not set.');
257+ }
254258}
255259
256260/**
public/scripts/extensions/stable-diffusion/index.js+116 -0
@@ -82,6 +82,7 @@ const sources = {
8282 pollinations: 'pollinations',
8383 stability: 'stability',
8484 huggingface: 'huggingface',
85+ electronhub: 'electronhub',
8586 nanogpt: 'nanogpt',
8687 bfl: 'bfl',
8788 falai: 'falai',
@@ -1289,6 +1290,7 @@ async function onModelChange() {
12891290 sources.pollinations,
12901291 sources.stability,
12911292 sources.huggingface,
1293+ sources.electronhub,
12921294 sources.nanogpt,
12931295 sources.bfl,
12941296 sources.falai,
@@ -1506,6 +1508,9 @@ async function loadSamplers() {
15061508 case sources.huggingface:
15071509 samplers = ['N/A'];
15081510 break;
1511+ case sources.electronhub:
1512+ samplers = ['N/A'];
1513+ break;
15091514 case sources.nanogpt:
15101515 samplers = ['N/A'];
15111516 break;
@@ -1702,6 +1707,9 @@ async function loadModels() {
17021707 case sources.huggingface:
17031708 models = [{ value: '', text: '<Enter Model ID above>' }];
17041709 break;
1710+ case sources.electronhub:
1711+ models = await loadElectronHubModels();
1712+ break;
17051713 case sources.nanogpt:
17061714 models = await loadNanoGPTModels();
17071715 break;
@@ -1806,6 +1814,24 @@ async function loadTogetherAIModels() {
18061814 return [];
18071815}
18081816
1817+async function loadElectronHubModels() {
1818+ if (!secret_state[SECRET_KEYS.ELECTRONHUB]) {
1819+ console.debug('Electron Hub API key is not set.');
1820+ return [];
1821+ }
1822+
1823+ const result = await fetch('/api/sd/electronhub/models', {
1824+ method: 'POST',
1825+ headers: getRequestHeaders(),
1826+ });
1827+
1828+ if (result.ok) {
1829+ return await result.json();
1830+ }
1831+
1832+ return [];
1833+}
1834+
18091835async function loadNanoGPTModels() {
18101836 if (!secret_state[SECRET_KEYS.NANOGPT]) {
18111837 console.debug('NanoGPT API key is not set.');
@@ -2131,6 +2157,9 @@ async function loadSchedulers() {
21312157 case sources.huggingface:
21322158 schedulers = ['N/A'];
21332159 break;
2160+ case sources.electronhub:
2161+ schedulers = ['N/A'];
2162+ break;
21342163 case sources.nanogpt:
21352164 schedulers = ['N/A'];
21362165 break;
@@ -2228,6 +2257,9 @@ async function loadVaes() {
22282257 case sources.huggingface:
22292258 vaes = ['N/A'];
22302259 break;
2260+ case sources.electronhub:
2261+ vaes = ['N/A'];
2262+ break;
22312263 case sources.nanogpt:
22322264 vaes = ['N/A'];
22332265 break;
@@ -2811,6 +2843,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
28112843 case sources.huggingface:
28122844 result = await generateHuggingFaceImage(prefixedPrompt, signal);
28132845 break;
2846+ case sources.electronhub:
2847+ result = await generateElectronHubImage(prefixedPrompt, signal);
2848+ break;
28142849 case sources.nanogpt:
28152850 result = await generateNanoGPTImage(prefixedPrompt, negativePrompt, signal);
28162851 break;
@@ -3014,6 +3049,56 @@ function getClosestAspectRatio(width, height, source) {
30143049}
30153050
30163051/**
3052+ * Get closest size for Electron Hub
3053+ * @param {number} width - The width of the image
3054+ * @param {number} height - The height of the image
3055+ * @returns {Promise<string>} - The closest size
3056+ */
3057+async function getClosestSize(width, height) {
3058+ const response = await fetch('/api/sd/electronhub/sizes', {
3059+ method: 'POST',
3060+ headers: getRequestHeaders(),
3061+ body: JSON.stringify({
3062+ model: extension_settings.sd.model,
3063+ }),
3064+ });
3065+ if (!response.ok) {
3066+ const text = await response.text();
3067+ throw new Error(text);
3068+ }
3069+ const result = await response.json();
3070+ const sizesData = result.sizes;
3071+
3072+ const closestSize = sizesData.reduce((closest, size) => {
3073+ if (!size || typeof size !== 'string') {
3074+ return closest;
3075+ }
3076+ const sizeParts = size.split('x');
3077+ if (sizeParts.length !== 2) {
3078+ return closest;
3079+ }
3080+
3081+ const sizeWidth = Number(sizeParts[0]);
3082+ const sizeHeight = Number(sizeParts[1]);
3083+ const targetWidth = Number(width);
3084+ const targetHeight = Number(height);
3085+
3086+ if (isNaN(sizeWidth) || isNaN(sizeHeight) || isNaN(targetWidth) || isNaN(targetHeight)) {
3087+ return closest;
3088+ }
3089+
3090+ const sizeArea = sizeWidth * sizeHeight;
3091+ const targetArea = targetWidth * targetHeight;
3092+ const diff = Math.abs(sizeArea - targetArea);
3093+
3094+ return diff < closest.diff ? { size, diff } : closest;
3095+ }, { size: null, diff: Infinity });
3096+
3097+ const size = closestSize.size;
3098+ return size;
3099+}
3100+
3101+/**
30173102 * Generates an image using Stability AI.
30183103 * @param {string} prompt - The main instruction used to guide the image generation.
30193104 * @param {string} negativePrompt - The instruction used to restrict the image generation.
@@ -3565,6 +3650,35 @@ async function generateHuggingFaceImage(prompt, signal) {
35653650}
35663651
35673652/**
3653+ * Generates an image using the Electron Hub API.
3654+ * @param {string} prompt - The main instruction used to guide the image generation.
3655+ * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3656+ * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3657+ */
3658+async function generateElectronHubImage(prompt, signal) {
3659+ const size = await getClosestSize(extension_settings.sd.width, extension_settings.sd.height);
3660+
3661+ const result = await fetch('/api/sd/electronhub/generate', {
3662+ method: 'POST',
3663+ headers: getRequestHeaders(),
3664+ signal: signal,
3665+ body: JSON.stringify({
3666+ model: extension_settings.sd.model,
3667+ prompt: prompt,
3668+ size: size,
3669+ }),
3670+ });
3671+
3672+ if (result.ok) {
3673+ const data = await result.json();
3674+ return { format: 'jpg', data: data.image };
3675+ } else {
3676+ const text = await result.text();
3677+ throw new Error(text);
3678+ }
3679+}
3680+
3681+/**
35683682 * Generates an image using the NanoGPT API.
35693683 * @param {string} prompt - The main instruction used to guide the image generation.
35703684 * @param {string} negativePrompt - The instruction used to restrict the image generation.
@@ -4014,6 +4128,8 @@ function isValidState() {
40144128 return secret_state[SECRET_KEYS.STABILITY];
40154129 case sources.huggingface:
40164130 return secret_state[SECRET_KEYS.HUGGINGFACE];
4131+ case sources.electronhub:
4132+ return secret_state[SECRET_KEYS.ELECTRONHUB];
40174133 case sources.nanogpt:
40184134 return secret_state[SECRET_KEYS.NANOGPT];
40194135 case sources.bfl:
public/scripts/extensions/stable-diffusion/settings.html+4 -0
@@ -41,6 +41,7 @@
4141 <option value="bfl">BFL (Black Forest Labs)</option>
4242 <option value="comfy">ComfyUI</option>
4343 <option value="drawthings">DrawThings HTTP API</option>
44+ <option value="electronhub">Electron Hub</option>
4445 <option value="extras">Extras API (deprecated)</option>
4546 <option value="falai">FAL.AI</option>
4647 <option value="google">Google AI</option>
@@ -93,6 +94,9 @@
9394 <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label>
9495 <input id="sd_huggingface_model_id" type="text" class="text_pole" data-i18n="[placeholder]e.g. black-forest-labs/FLUX.1-dev" placeholder="e.g. black-forest-labs/FLUX.1-dev" value="" />
9596 </div>
97+ <div data-sd-source="electronhub">
98+ <i>Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.</i>
99+ </div>
96100 <div data-sd-source="nanogpt">
97101 <i>Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.</i>
98102 </div>
public/scripts/openai.js+119 -1
@@ -179,6 +179,7 @@ export const chat_completion_sources = {
179179 COHERE: 'cohere',
180180 PERPLEXITY: 'perplexity',
181181 GROQ: 'groq',
182+ ELECTRONHUB: 'electronhub',
182183 NANOGPT: 'nanogpt',
183184 DEEPSEEK: 'deepseek',
184185 AIMLAPI: 'aimlapi',
@@ -271,6 +272,7 @@ export const settingsToUpdate = {
271272 cohere_model: ['#model_cohere_select', 'cohere_model', false, true],
272273 perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true],
273274 groq_model: ['#model_groq_select', 'groq_model', false, true],
275+ electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true],
274276 nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true],
275277 deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true],
276278 aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true],
@@ -369,6 +371,7 @@ const default_settings = {
369371 cohere_model: 'command-r-plus',
370372 perplexity_model: 'sonar-pro',
371373 groq_model: 'llama-3.3-70b-versatile',
374+ electronhub_model: 'gpt-4o-mini',
372375 nanogpt_model: 'gpt-4o-mini',
373376 deepseek_model: 'deepseek-chat',
374377 aimlapi_model: 'gpt-4o-mini-2024-07-18',
@@ -458,6 +461,7 @@ const oai_settings = {
458461 cohere_model: 'command-r-plus',
459462 perplexity_model: 'sonar-pro',
460463 groq_model: 'llama-3.1-70b-versatile',
464+ electronhub_model: 'gpt-4o-mini',
461465 nanogpt_model: 'gpt-4o-mini',
462466 deepseek_model: 'deepseek-chat',
463467 aimlapi_model: 'gpt-4-turbo',
@@ -1542,6 +1546,11 @@ export function tryParseStreamingError(response, decoded, { quiet = false } = {}
15421546 !quiet && toastr.error(data.message, 'Chat Completion API');
15431547 throw new Error(data);
15441548 }
1549+
1550+ if (data.detail) {
1551+ !quiet && toastr.error(data.detail?.error?.message || response.statusText, 'Chat Completion API');
1552+ throw new Error(data);
1553+ }
15451554 }
15461555 catch {
15471556 // No JSON. Do nothing.
@@ -1614,6 +1623,8 @@ export function getChatCompletionModel(source = null) {
16141623 return oai_settings.perplexity_model;
16151624 case chat_completion_sources.GROQ:
16161625 return oai_settings.groq_model;
1626+ case chat_completion_sources.ELECTRONHUB:
1627+ return oai_settings.electronhub_model;
16171628 case chat_completion_sources.NANOGPT:
16181629 return oai_settings.nanogpt_model;
16191630 case chat_completion_sources.DEEPSEEK:
@@ -1774,6 +1785,26 @@ function saveModelList(data) {
17741785 }
17751786 }
17761787
1788+ if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
1789+ $('#model_electronhub_select').empty();
1790+ model_list.forEach((model) => {
1791+ if (model?.endpoints?.includes('/v1/chat/completions')) {
1792+ $('#model_electronhub_select').append(
1793+ $('<option>', {
1794+ value: model.id,
1795+ text: model.name,
1796+ }));
1797+ }
1798+ });
1799+
1800+ const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model);
1801+ if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) {
1802+ oai_settings.electronhub_model = model_list[0].id;
1803+ }
1804+
1805+ $('#model_electronhub_select').val(oai_settings.electronhub_model).trigger('change');
1806+ }
1807+
17771808 if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
17781809 $('#model_nanogpt_select').empty();
17791810 model_list.forEach((model) => {
@@ -2044,12 +2075,14 @@ function getReasoningEffort() {
20442075 chat_completion_sources.POLLINATIONS,
20452076 chat_completion_sources.PERPLEXITY,
20462077 chat_completion_sources.COMETAPI,
2078+ chat_completion_sources.ELECTRONHUB,
20472079 ];
20482080
20492081 if (!reasoningEffortSources.includes(oai_settings.chat_completion_source)) {
20502082 return oai_settings.reasoning_effort;
20512083 }
20522084
2085+ function resolveReasoningEffort() {
20532086 switch (oai_settings.reasoning_effort) {
20542087 case reasoning_effort_types.auto:
20552088 return undefined;
@@ -2064,6 +2097,23 @@ function getReasoningEffort() {
20642097 }
20652098 }
20662099
2100+ const reasoningEffort = resolveReasoningEffort();
2101+
2102+ // Check if the resolved effort supported by the model
2103+ if (oai_settings.chat_completion_source === chat_completion_sources.ELECTRONHUB) {
2104+ if (Array.isArray(model_list) && reasoningEffort) {
2105+ const currentModel = model_list.find(m => m.id === oai_settings.electronhub_model);
2106+ const supportedEfforts = currentModel?.metadata?.supported_reasoning_efforts;
2107+ if (Array.isArray(supportedEfforts) && supportedEfforts.includes(reasoningEffort)) {
2108+ return reasoningEffort;
2109+ }
2110+ return undefined;
2111+ }
2112+ }
2113+
2114+ return reasoningEffort;
2115+}
2116+
20672117/**
20682118 * Send a chat completion request to backend
20692119 * @param {string} type (impersonate, quiet, continue, etc)
@@ -2100,6 +2150,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
21002150 const isGroq = oai_settings.chat_completion_source == chat_completion_sources.GROQ;
21012151 const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
21022152 const isAimlapi = oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI;
2153+ const isElectronHub = oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB;
21032154 const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
21042155 const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;
21052156 const isMoonshot = oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT;
@@ -2283,6 +2334,11 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
22832334 delete generate_data.max_tokens;
22842335 }
22852336
2337+ // https://docs.electronhub.ai/api-reference/chat/completions
2338+ if (isElectronHub) {
2339+ generate_data['top_k'] = Number(oai_settings.top_k_openai);
2340+ }
2341+
22862342 const seedSupportedSources = [
22872343 chat_completion_sources.OPENAI,
22882344 chat_completion_sources.OPENROUTER,
@@ -2290,6 +2346,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
22902346 chat_completion_sources.CUSTOM,
22912347 chat_completion_sources.COHERE,
22922348 chat_completion_sources.GROQ,
2349+ chat_completion_sources.ELECTRONHUB,
22932350 chat_completion_sources.NANOGPT,
22942351 chat_completion_sources.XAI,
22952352 chat_completion_sources.POLLINATIONS,
@@ -2462,7 +2519,7 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
24622519 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
24632520 }
24642521 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
24652522 } else if ([chat_completion_sources.CUSTOM, chat_completion_sources.POLLINATIONS, chat_completion_sources.AIMLAPI, chat_completion_sources.MOONSHOT, chat_completion_sources.COMETAPI, chat_completion_sources.ELECTRONHUB].includes(chat_completion_source)) {
24662523 if (show_thoughts) {
24672524 state.reasoning +=
24682525 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
@@ -3424,6 +3481,7 @@ function loadOpenAISettings(data, settings) {
34243481 oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model;
34253482 oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model;
34263483 oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model;
3484+ oai_settings.electronhub_model = settings.electronhub_model ?? default_settings.electronhub_model;
34273485 oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;
34283486 oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;
34293487 oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model;
@@ -3519,6 +3577,8 @@ function loadOpenAISettings(data, settings) {
35193577 $(`#model_perplexity_select option[value="${oai_settings.perplexity_model}"`).prop('selected', true);
35203578 $('#model_groq_select').val(oai_settings.groq_model);
35213579 $(`#model_groq_select option[value="${oai_settings.groq_model}"`).prop('selected', true);
3580+ $('#model_electronhub_select').val(oai_settings.electronhub_model);
3581+ $(`#model_electronhub_select option[value="${oai_settings.electronhub_model}"`).prop('selected', true);
35223582 $('#model_nanogpt_select').val(oai_settings.nanogpt_model);
35233583 $(`#model_nanogpt_select option[value="${oai_settings.nanogpt_model}"`).prop('selected', true);
35243584 $('#model_deepseek_select').val(oai_settings.deepseek_model);
@@ -3805,6 +3865,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
38053865 xai_model: settings.xai_model,
38063866 pollinations_model: settings.pollinations_model,
38073867 aimlapi_model: settings.aimlapi_model,
3868+ electronhub_model: settings.electronhub_model,
38083869 moonshot_model: settings.moonshot_model,
38093870 fireworks_model: settings.fireworks_model,
38103871 cometapi_model: settings.cometapi_model,
@@ -4571,6 +4632,26 @@ function getFireworksMaxContext(model, isUnlocked) {
45714632}
45724633
45734634/**
4635+ * Get the maximum context size for the ElectronHub model
4636+ * @param {string} model Model identifier
4637+ * @param {boolean} isUnlocked Whether context limits are unlocked
4638+ * @returns {number} Maximum context size in tokens
4639+ */
4640+function getElectronHubMaxContext(model, isUnlocked) {
4641+ if (isUnlocked) {
4642+ return unlocked_max;
4643+ }
4644+
4645+ if (Array.isArray(model_list)) {
4646+ const modelInfo = model_list.find(m => m.id === model);
4647+ if (modelInfo?.tokens) {
4648+ return modelInfo.tokens;
4649+ }
4650+ }
4651+ return max_8k;
4652+}
4653+
4654+/**
45744655 * Get the maximum context size for the NanoGPT model
45754656 * @param {string} model Model identifier
45764657 * @param {boolean} isUnlocked Whether context limits are unlocked
@@ -4681,6 +4762,15 @@ async function onModelChange() {
46814762 oai_settings.groq_model = value;
46824763 }
46834764
4765+ if ($(this).is('#model_electronhub_select')) {
4766+ if (!value) {
4767+ console.debug('Null ElectronHub model selected. Ignoring.');
4768+ return;
4769+ }
4770+ console.log('ElectronHub model changed to', value);
4771+ oai_settings.electronhub_model = value;
4772+ }
4773+
46844774 if ($(this).is('#model_nanogpt_select')) {
46854775 if (!value) {
46864776 console.debug('Null NanoGPT model selected. Ignoring.');
@@ -4922,6 +5012,15 @@ async function onModelChange() {
49225012 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
49235013 }
49245014
5015+ if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
5016+ const maxContext = getElectronHubMaxContext(oai_settings.electronhub_model, oai_settings.max_context_unlocked);
5017+ $('#openai_max_context').attr('max', maxContext);
5018+ oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5019+ $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5020+ oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5021+ $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5022+ }
5023+
49255024 if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) {
49265025 const maxContext = getNanoGptMaxContext(oai_settings.nanogpt_model, oai_settings.max_context_unlocked);
49275026 $('#openai_max_context').attr('max', maxContext);
@@ -5216,6 +5315,19 @@ async function onConnectButtonClick(e) {
52165315 }
52175316 }
52185317
5318+ if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
5319+ const api_key_electronhub = String($('#api_key_electronhub').val()).trim();
5320+
5321+ if (api_key_electronhub.length) {
5322+ await writeSecret(SECRET_KEYS.ELECTRONHUB, api_key_electronhub);
5323+ }
5324+
5325+ if (!secret_state[SECRET_KEYS.ELECTRONHUB]) {
5326+ console.log('No secret key saved for Electron Hub');
5327+ return;
5328+ }
5329+ }
5330+
52195331 if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
52205332 const api_key_nanogpt = String($('#api_key_nanogpt').val()).trim();
52215333
@@ -5350,6 +5462,9 @@ function toggleChatCompletionForms() {
53505462 else if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
53515463 $('#model_groq_select').trigger('change');
53525464 }
5465+ else if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
5466+ $('#model_electronhub_select').trigger('change');
5467+ }
53535468 else if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
53545469 $('#model_nanogpt_select').trigger('change');
53555470 }
@@ -5518,6 +5633,8 @@ export function isImageInliningSupported() {
55185633 return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
55195634 case chat_completion_sources.AIMLAPI:
55205635 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.aimlapi_model)?.features?.includes('openai/chat-completion.vision'));
5636+ case chat_completion_sources.ELECTRONHUB:
5637+ return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.electronhub_model)?.metadata?.vision);
55215638 case chat_completion_sources.POLLINATIONS:
55225639 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.vision);
55235640 case chat_completion_sources.COMETAPI:
@@ -6326,6 +6443,7 @@ export function initOpenAI() {
63266443 $('#model_cohere_select').on('change', onModelChange);
63276444 $('#model_perplexity_select').on('change', onModelChange);
63286445 $('#model_groq_select').on('change', onModelChange);
6446+ $('#model_electronhub_select').on('change', onModelChange);
63296447 $('#model_nanogpt_select').on('change', onModelChange);
63306448 $('#model_deepseek_select').on('change', onModelChange);
63316449 $('#model_aimlapi_select').on('change', onModelChange);
public/scripts/secrets.js+3 -0
@@ -51,6 +51,7 @@ export const SECRET_KEYS = {
5151 HUGGINGFACE: 'api_key_huggingface',
5252 STABILITY: 'api_key_stability',
5353 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
54+ ELECTRONHUB: 'api_key_electronhub',
5455 NANOGPT: 'api_key_nanogpt',
5556 TAVILY: 'api_key_tavily',
5657 BFL: 'api_key_bfl',
@@ -95,6 +96,7 @@ const FRIENDLY_NAMES = {
9596 [SECRET_KEYS.GROQ]: 'Groq',
9697 [SECRET_KEYS.FEATHERLESS]: 'Featherless',
9798 [SECRET_KEYS.HUGGINGFACE]: 'HuggingFace',
99+ [SECRET_KEYS.ELECTRONHUB]: 'Electron Hub',
98100 [SECRET_KEYS.NANOGPT]: 'NanoGPT',
99101 [SECRET_KEYS.GENERIC]: 'Generic (OpenAI-compatible)',
100102 [SECRET_KEYS.DEEPSEEK]: 'DeepSeek',
@@ -148,6 +150,7 @@ const INPUT_MAP = {
148150 [SECRET_KEYS.GROQ]: '#api_key_groq',
149151 [SECRET_KEYS.FEATHERLESS]: '#api_key_featherless',
150152 [SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface',
153+ [SECRET_KEYS.ELECTRONHUB]: '#api_key_electronhub',
151154 [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
152155 [SECRET_KEYS.GENERIC]: '#api_key_generic',
153156 [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',
public/scripts/slash-commands.js+1 -0
@@ -4841,6 +4841,7 @@ function getModelOptions(quiet) {
48414841 { id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
48424842 { id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
48434843 { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
4844+ { id: 'model_electronhub_select', api: 'openai', type: chat_completion_sources.ELECTRONHUB },
48444845 { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
48454846 { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
48464847 { id: 'model_aimlapi_select', api: 'openai', type: chat_completion_sources.AIMLAPI },
public/scripts/tool-calling.js+8 -0
@@ -633,6 +633,13 @@ export class ToolManager {
633633 }
634634 }
635635
636+ if (oai_settings.chat_completion_source === chat_completion_sources.ELECTRONHUB && Array.isArray(model_list)) {
637+ const currentModel = model_list.find(model => model.id === oai_settings.electronhub_model);
638+ if (currentModel && currentModel.metadata?.function_call) {
639+ return currentModel.metadata.function_call;
640+ }
641+ }
642+
636643 const supportedSources = [
637644 chat_completion_sources.OPENAI,
638645 chat_completion_sources.CUSTOM,
@@ -651,6 +658,7 @@ export class ToolManager {
651658 chat_completion_sources.MOONSHOT,
652659 chat_completion_sources.FIREWORKS,
653660 chat_completion_sources.COMETAPI,
661+ chat_completion_sources.ELECTRONHUB,
654662 ];
655663 return supportedSources.includes(oai_settings.chat_completion_source);
656664 }
src/constants.js+1 -0
@@ -173,6 +173,7 @@ export const CHAT_COMPLETION_SOURCES = {
173173 COHERE: 'cohere',
174174 PERPLEXITY: 'perplexity',
175175 GROQ: 'groq',
176+ ELECTRONHUB: 'electronhub',
176177 NANOGPT: 'nanogpt',
177178 DEEPSEEK: 'deepseek',
178179 AIMLAPI: 'aimlapi',
src/endpoints/backends/chat-completions.js+124 -0
@@ -61,6 +61,7 @@ const API_GROQ = 'https://api.groq.com/openai/v1';
6161const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
6262const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com';
6363const API_AI21 = 'https://api.ai21.com/studio/v1';
64+const API_ELECTRONHUB = 'https://api.electronhub.ai/v1';
6465const API_NANOGPT = 'https://nano-gpt.com/api/v1';
6566const API_DEEPSEEK = 'https://api.deepseek.com/beta';
6667const API_XAI = 'https://api.x.ai/v1';
@@ -1193,6 +1194,106 @@ async function sendAimlapiRequest(request, response) {
11931194 }
11941195}
11951196
1197+/**
1198+ * Sends a request to Electron Hub.
1199+ * @param {express.Request} request Express request
1200+ * @param {express.Response} response Express response
1201+ */
1202+async function sendElectronHubRequest(request, response) {
1203+ const apiUrl = API_ELECTRONHUB;
1204+ const apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
1205+
1206+ if (!apiKey) {
1207+ console.warn('Electron Hub key is missing.');
1208+ return response.status(400).send({ error: true });
1209+ }
1210+
1211+ const controller = new AbortController();
1212+ request.socket.removeAllListeners('close');
1213+ request.socket.on('close', function () {
1214+ controller.abort();
1215+ });
1216+
1217+ try {
1218+ let bodyParams = {};
1219+
1220+ if (request.body.enable_web_search) {
1221+ bodyParams['web_search'] = true;
1222+ }
1223+
1224+ if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
1225+ bodyParams['tools'] = request.body.tools;
1226+ bodyParams['tool_choice'] = request.body.tool_choice;
1227+ }
1228+
1229+ if (request.body.reasoning_effort) {
1230+ bodyParams['reasoning_effort'] = request.body.reasoning_effort;
1231+ }
1232+
1233+ if (request.body.json_schema) {
1234+ bodyParams['response_format'] = {
1235+ type: 'json_schema',
1236+ json_schema: {
1237+ name: request.body.json_schema.name,
1238+ description: request.body.json_schema.description,
1239+ schema: request.body.json_schema.value,
1240+ strict: request.body.json_schema.strict ?? true,
1241+ },
1242+ };
1243+ }
1244+
1245+ const requestBody = {
1246+ 'messages': request.body.messages,
1247+ 'model': request.body.model,
1248+ 'temperature': request.body.temperature,
1249+ 'max_tokens': request.body.max_tokens,
1250+ 'stream': request.body.stream,
1251+ 'presence_penalty': request.body.presence_penalty,
1252+ 'frequency_penalty': request.body.frequency_penalty,
1253+ 'top_p': request.body.top_p,
1254+ 'top_k': request.body.top_k,
1255+ 'seed': request.body.seed,
1256+ ...bodyParams,
1257+ };
1258+
1259+ const config = {
1260+ method: 'POST',
1261+ headers: {
1262+ 'Content-Type': 'application/json',
1263+ 'Authorization': 'Bearer ' + apiKey,
1264+ },
1265+ body: JSON.stringify(requestBody),
1266+ signal: controller.signal,
1267+ };
1268+
1269+ console.debug('Electron Hub request:', requestBody);
1270+
1271+ const generateResponse = await fetch(apiUrl + '/chat/completions', config);
1272+
1273+ if (request.body.stream) {
1274+ forwardFetchResponse(generateResponse, response);
1275+ } else {
1276+ if (!generateResponse.ok) {
1277+ const errorText = await generateResponse.text();
1278+ console.warn('Electron Hub returned error: ', errorText);
1279+ const errorJson = tryParse(errorText) ?? { error: true };
1280+ return response.status(500).send(errorJson);
1281+ }
1282+ const generateResponseJson = await generateResponse.json();
1283+ console.debug('Electron Hub response:', generateResponseJson);
1284+ return response.send(generateResponseJson);
1285+ }
1286+ }
1287+ catch (error) {
1288+ console.error('Error communicating with Electron Hub: ', error);
1289+ if (!response.headersSent) {
1290+ response.send({ error: true });
1291+ } else {
1292+ response.end();
1293+ }
1294+ }
1295+}
1296+
11961297export const router = express.Router();
11971298
11981299router.post('/status', async function (request, statusResponse) {
@@ -1225,6 +1326,10 @@ router.post('/status', async function (request, statusResponse) {
12251326 apiUrl = API_COHERE_V1;
12261327 apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE);
12271328 headers = {};
1329+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ELECTRONHUB) {
1330+ apiUrl = API_ELECTRONHUB;
1331+ apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
1332+ headers = {};
12281333 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) {
12291334 apiUrl = API_NANOGPT;
12301335 apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
@@ -1490,6 +1595,7 @@ router.post('/generate', function (request, response) {
14901595 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
14911596 case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response);
14921597 case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
1598+ case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return sendElectronHubRequest(request, response);
14931599 }
14941600
14951601 let apiUrl;
@@ -1927,4 +2033,22 @@ multimodalModels.post('/nanogpt', async (_req, res) => {
19272033 }
19282034});
19292035
2036+multimodalModels.post('/electronhub', async (_req, res) => {
2037+ try {
2038+ const response = await fetch('https://api.electronhub.ai/v1/models');
2039+
2040+ if (!response.ok) {
2041+ return res.json([]);
2042+ }
2043+
2044+ /** @type {any} */
2045+ const data = await response.json();
2046+ const multimodalModels = data.data.filter(m => m.metadata?.vision).map(m => m.id);
2047+ return res.json(multimodalModels);
2048+ } catch (error) {
2049+ console.error(error);
2050+ return res.sendStatus(500);
2051+ }
2052+});
2053+
19302054router.use('/multimodal-models', multimodalModels);
src/endpoints/openai.js+8 -0
@@ -81,6 +81,10 @@ router.post('/caption-image', async (request, response) => {
8181 key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
8282 }
8383
84+ if (request.body.api === 'electronhub') {
85+ key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
86+ }
87+
8488 const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp', 'pollinations'];
8589 if (!key && !request.body.reverse_proxy && !noKeyTypes.includes(request.body.api)) {
8690 console.warn('No key found for API', request.body.api);
@@ -169,6 +173,10 @@ router.post('/caption-image', async (request, response) => {
169173 apiUrl = 'https://nano-gpt.com/api/v1/chat/completions';
170174 }
171175
176+ if (request.body.api === 'electronhub') {
177+ apiUrl = 'https://api.electronhub.ai/v1/chat/completions';
178+ }
179+
172180 if (['koboldcpp', 'vllm', 'llamacpp', 'ooba'].includes(request.body.api)) {
173181 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;
174182 }
src/endpoints/secrets.js+1 -0
@@ -45,6 +45,7 @@ export const SECRET_KEYS = {
4545 STABILITY: 'api_key_stability',
4646 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
4747 TAVILY: 'api_key_tavily',
48+ ELECTRONHUB: 'api_key_electronhub',
4849 NANOGPT: 'api_key_nanogpt',
4950 BFL: 'api_key_bfl',
5051 FALAI: 'api_key_falai',
src/endpoints/stable-diffusion.js+113 -0
@@ -957,6 +957,118 @@ huggingface.post('/generate', async (request, response) => {
957957 }
958958});
959959
960+const electronhub = express.Router();
961+
962+electronhub.post('/models', async (request, response) => {
963+ try {
964+ const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
965+
966+ if (!key) {
967+ console.warn('Electron Hub key not found.');
968+ return response.sendStatus(400);
969+ }
970+
971+ const modelsResponse = await fetch('https://api.electronhub.ai/v1/models', {
972+ method: 'GET',
973+ headers: {
974+ 'Authorization': `Bearer ${key}`,
975+ 'Content-Type': 'application/json',
976+ },
977+ });
978+
979+ if (!modelsResponse.ok) {
980+ console.warn('Electron Hub returned an error.');
981+ return response.sendStatus(500);
982+ }
983+
984+ /** @type {any} */
985+ const data = await modelsResponse.json();
986+ const models = data.data.filter(x => x.endpoints.includes('/v1/images/generations')).map(x => ({ value: x.id, text: x.name }));
987+ return response.send(models);
988+ } catch (error) {
989+ console.error(error);
990+ return response.sendStatus(500);
991+ }
992+});
993+
994+electronhub.post('/generate', async (request, response) => {
995+ try {
996+ const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
997+
998+ if (!key) {
999+ console.warn('Electron Hub key not found.');
1000+ return response.sendStatus(400);
1001+ }
1002+
1003+ let bodyParams = {
1004+ model: request.body.model,
1005+ prompt: request.body.prompt,
1006+ response_format: 'b64_json',
1007+ };
1008+
1009+ if (request.body.size) {
1010+ bodyParams.size = request.body.size;
1011+ }
1012+
1013+ const result = await fetch('https://api.electronhub.ai/v1/images/generations', {
1014+ method: 'POST',
1015+ headers: {
1016+ 'Authorization': `Bearer ${key}`,
1017+ 'Content-Type': 'application/json',
1018+ },
1019+ body: JSON.stringify({
1020+ ...bodyParams,
1021+ }),
1022+ });
1023+
1024+ if (!result.ok) {
1025+ const errorText = await result.text();
1026+ console.warn('Electron Hub returned an error.', result.status, result.statusText, errorText);
1027+ return response.sendStatus(500);
1028+ }
1029+
1030+ /** @type {any} */
1031+ const data = await result.json();
1032+ const image = data?.data?.[0]?.b64_json;
1033+
1034+ if (!image) {
1035+ console.warn('Electron Hub returned invalid data.');
1036+ return response.sendStatus(500);
1037+ }
1038+
1039+ return response.send({ image });
1040+ } catch (error) {
1041+ console.error(error);
1042+ return response.sendStatus(500);
1043+ }
1044+});
1045+
1046+electronhub.post('/sizes', async (request, response) => {
1047+ const result = await fetch(`https://api.electronhub.ai/v1/models/${request.body.model}`, {
1048+ method: 'GET',
1049+ headers: {
1050+ 'Content-Type': 'application/json',
1051+ },
1052+ });
1053+
1054+ if (!result.ok) {
1055+ console.warn('Electron Hub returned an error.');
1056+ return response.sendStatus(500);
1057+ }
1058+
1059+ /** @type {any} */
1060+ const data = await result.json();
1061+
1062+ const sizes = data.sizes;
1063+
1064+ if (!sizes) {
1065+ console.warn('Electron Hub returned invalid data.');
1066+ return response.sendStatus(500);
1067+ }
1068+
1069+ return response.send({ sizes });
1070+});
1071+
9601072const nanogpt = express.Router();
9611073
9621074nanogpt.post('/models', async (request, response) => {
@@ -1439,6 +1551,7 @@ router.use('/drawthings', drawthings);
14391551router.use('/pollinations', pollinations);
14401552router.use('/stability', stability);
14411553router.use('/huggingface', huggingface);
1554+router.use('/electronhub', electronhub);
14421555router.use('/nanogpt', nanogpt);
14431556router.use('/bfl', bfl);
14441557router.use('/falai', falai);