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
+1
View File
@@ -0,0 +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>

After

Width:  |  Height:  |  Size: 589 B

+28 -13
View File
@@ -691,7 +691,7 @@
</span>
</div>
</div>
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
<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">
<div class="range-block-title" data-i18n="Temperature">
Temperature
</div>
@@ -704,7 +704,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
<div class="range-block-title" data-i18n="Frequency Penalty">
Frequency Penalty
</div>
@@ -717,7 +717,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
<div class="range-block-title" data-i18n="Presence Penalty">
Presence Penalty
</div>
@@ -730,7 +730,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity">
<div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub">
<div class="range-block-title" data-i18n="Top K">
Top K
</div>
@@ -743,7 +743,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
<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">
<div class="range-block-title" data-i18n="Top P">
Top P
</div>
@@ -980,7 +980,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai">
<div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai">
<div class="range-block-title justifyLeft" data-i18n="Seed">
Seed
</div>
@@ -1970,7 +1970,7 @@
</span>
</div>
</div>
<div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,nanogpt">
<div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,electronhub,nanogpt">
<label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_enable_web_search" type="checkbox" />
<span data-i18n="Enable web search">Enable web search</span>
@@ -1984,7 +1984,7 @@
</b>
</div>
</div>
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi">
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub">
<label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_function_calling" type="checkbox" />
<span data-i18n="Enable function calling">Enable function calling</span>
@@ -1999,7 +1999,7 @@
<strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
</div>
</div>
<div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt">
<div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub">
<label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_image_inlining" type="checkbox" />
<span data-i18n="Send inline images">Send inline images</span>
@@ -2015,7 +2015,7 @@
<code><i class="fa-solid fa-wand-magic-sparkles"></i></code>
<span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
</div>
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai">
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub">
<div class="flex-container oneline-dropdown">
<label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
Inline Image Quality
@@ -2077,7 +2077,7 @@
</span>
</div>
</div>
<div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi">
<div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub">
<label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
<input id="openai_show_thoughts" type="checkbox" />
<span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2091,7 +2091,7 @@
</span>
</div>
</div>
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi">
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub">
<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.">
<label for="openai_reasoning_effort">
<span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2105,7 +2105,7 @@
<option data-i18n="openai_reasoning_effort_high" value="high">High</option>
<option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
</select>
<div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,perplexity" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">
<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.">
OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
</div>
<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 @@
<!-- Temporarily disabled. -->
<!-- <option value="cometapi">CometAPI</option> -->
<option value="deepseek">DeepSeek</option>
<option value="electronhub">Electron Hub</option>
<option value="fireworks">Fireworks AI</option>
<option value="groq">Groq</option>
<option value="makersuite">Google AI Studio</option>
@@ -3469,6 +3470,20 @@
<option value="mistral-saba-24b">mistral-saba-24b</option>
</select>
</div>
<div id="electronhub_form" data-source="electronhub">
<h4 data-i18n="Electron Hub API Key">Electron Hub API Key</h4>
<div class="flex-container">
<input id="api_key_electronhub" name="api_key_electronhub" class="text_pole flex1" value="" type="text" autocomplete="off">
<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>
</div>
<div data-for="api_key_electronhub" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
For privacy reasons, your API key will be hidden after you click 'Connect'.
</div>
<h4 data-i18n="Electron Hub Model">Electron Hub Model</h4>
<select id="model_electronhub_select">
<option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
</select>
</div>
<div id="nanogpt_form" data-source="nanogpt">
<h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4>
<div class="flex-container">
+2
View File
@@ -317,6 +317,8 @@
"flag": "وضع علامة",
"API key (optional)": "مفتاح API (اختياري)",
"Server url": "رابط الخادم",
"Electron Hub API Key": "مفتاح API لـ Electron Hub",
"Electron Hub Model": "نموذج Electron Hub",
"Example: http://127.0.0.1:5000": "مثال: http://127.0.0.1:5000",
"Custom model (optional)": "نموذج مخصص (اختياري)",
"vllm-project/vllm": "vllm-project/vllm (وضع غلاف OpenAI API)",
+2
View File
@@ -317,6 +317,8 @@
"flag": "Flagge",
"API key (optional)": "API-Schlüssel (optional)",
"Server url": "Server-URL",
"Electron Hub API Key": "Electron Hub API-Schlüssel",
"Electron Hub Model": "Electron Hub-Modell",
"Example: http://127.0.0.1:5000": "Beispiel: http://127.0.0.1:5000",
"Custom model (optional)": "Benutzerdefiniertes Modell (optional)",
"vllm-project/vllm": "vllm-project/vllm (OpenAI API-Wrappermodus)",
+2
View File
@@ -317,6 +317,8 @@
"flag": "bandera",
"API key (optional)": "Clave API (opcional)",
"Server url": "URL del servidor",
"Electron Hub API Key": "Clave API de Electron Hub",
"Electron Hub Model": "Modelo de Electron Hub",
"Example: http://127.0.0.1:5000": "Ejemplo: http://127.0.0.1:5000",
"Custom model (optional)": "Modelo personalizado (opcional)",
"vllm-project/vllm": "vllm-project/vllm (modo contenedor de API OpenAI)",
+2
View File
@@ -1411,6 +1411,8 @@
"Do not proceed if you do not agree to this!": "Ne continuez pas si vous n'êtes pas d'accord avec cela !",
"Claude API Key": "Clé API Claude",
"Allow fallback models": "Autoriser les modèles de secours",
"Electron Hub API Key": "Clé API Electron Hub",
"Electron Hub Model": "Modèle Electron Hub",
"NanoGPT API Key": "Clé API NanoGPT",
"NanoGPT Model": "Modèle NanoGPT",
"DeepSeek API Key": "Clé API DeepSeek",
+2
View File
@@ -317,6 +317,8 @@
"flag": "bandiera",
"API key (optional)": "Chiave API (opzionale)",
"Server url": "URL del server",
"Electron Hub API Key": "Chiave API di Electron Hub",
"Electron Hub Model": "Modello di Electron Hub",
"Example: http://127.0.0.1:5000": "Esempio: http://127.0.0.1:5000",
"Custom model (optional)": "Modello personalizzato (opzionale)",
"vllm-project/vllm": "vllm-project/vllm (modalità wrapper API OpenAI)",
+2
View File
@@ -317,6 +317,8 @@
"flag": "フラグ",
"API key (optional)": "APIキー(オプション)",
"Server url": "サーバーURL",
"Electron Hub API Key": "Electron Hub API キー",
"Electron Hub Model": "Electron Hub モデル",
"Example: http://127.0.0.1:5000": "例: http://127.0.0.1:5000",
"Custom model (optional)": "カスタムモデル(オプション)",
"vllm-project/vllm": "vllm-project/vllm (OpenAI API ラッパーモード)",
+2
View File
@@ -319,6 +319,8 @@
"flag": "깃발",
"API key (optional)": "API 키 (선택 사항)",
"Server url": "서버 URL",
"Electron Hub API Key": "Electron Hub API 키",
"Electron Hub Model": "Electron Hub 모델",
"Example: http://127.0.0.1:5000": "예시: http://127.0.0.1:5000",
"Custom model (optional)": "사용자 정의 모델 (선택 사항)",
"vllm-project/vllm": "vllm-project/vllm(OpenAI API 래퍼 모드)",
+2
View File
@@ -317,6 +317,8 @@
"flag": "bandeira",
"API key (optional)": "Chave da API (opcional)",
"Server url": "URL do servidor",
"Electron Hub API Key": "Chave API Electron Hub",
"Electron Hub Model": "Modelo Electron Hub",
"Example: http://127.0.0.1:5000": "Exemplo: http://127.0.0.1:5000",
"Custom model (optional)": "Modelo personalizado (opcional)",
"vllm-project/vllm": "vllm-project/vllm (modo wrapper da API OpenAI)",
+2
View File
@@ -1994,6 +1994,8 @@
"Click on the setting name to omit it from the profile.": "Нажмите на название настройки, чтобы исключить её из профиля",
"Included settings:": "Сохранённые параметры:",
"Server URL": "Адрес сервера",
"Electron Hub API Key": "Ключ от API Electron Hub",
"Electron Hub Model": "Модель Electron Hub",
"NanoGPT API Key": "Ключ от API NanoGPT",
"NanoGPT Model": "Модель NanoGPT",
"Use extension settings": "Использовать настройки из расширения",
+2
View File
@@ -317,6 +317,8 @@
"flag": "прапорцем",
"API key (optional)": "Ключ API (необов'язково)",
"Server url": "URL-адреса сервера",
"Electron Hub API Key": "Ключ API для Electron Hub",
"Electron Hub Model": "Модель Electron Hub",
"Example: http://127.0.0.1:5000": "Приклад: http://127.0.0.1:5000",
"Custom model (optional)": "Власна модель (необов'язково)",
"vllm-project/vllm": "vllm-project/vllm (режим оболонки OpenAI API)",
+2
View File
@@ -317,6 +317,8 @@
"flag": "cờ",
"API key (optional)": "Key API (tùy chọn)",
"Server url": "URL máy chủ",
"Electron Hub API Key": "Key API Electron Hub",
"Electron Hub Model": "Model Electron Hub",
"Example: http://127.0.0.1:5000": "Ví dụ: http://127.0.0.1:5000",
"Custom model (optional)": "Model tùy chỉnh (tùy chọn)",
"vllm-project/vllm": "vllm-project/vllm (Chế độ trình bao bọc API OpenAI)",
+2
View File
@@ -481,6 +481,8 @@
"MistralAI Model": "MistralAI 模型",
"Groq API Key": "Groq API 密钥",
"Groq Model": "Groq 模型",
"Electron Hub API Key": "Electron Hub API 密钥",
"Electron Hub Model": "Electron Hub 模型",
"NanoGPT API Key": "NanoGPT API 密钥",
"NanoGPT Model": "NanoGPT 模型",
"DeepSeek API Key": "DeepSeek API 密钥",
+2
View File
@@ -1791,6 +1791,8 @@
"Derive context size from backend": "從後端推導上下文大小",
"Using a proxy that you're not running yourself is a risk to your data privacy.": "使用非自行管理的代理服務可能導致您的資料隱私外洩。",
"Claude API Key": "Claude API 金鑰",
"Electron Hub API Key": "Electron Hub API 金鑰",
"Electron Hub Model": "Electron Hub 模型",
"NanoGPT API Key": "NanoGPT API 金鑰",
"NanoGPT Model": "NanoGPT 模型",
"context_derived": "若可能,根據模型後設資料推導。",
+1
View File
@@ -5369,6 +5369,7 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource
case chat_completion_sources.CUSTOM:
case chat_completion_sources.COHERE:
case chat_completion_sources.XAI:
case chat_completion_sources.ELECTRONHUB:
default:
result = tryParse(text);
break;
+1
View File
@@ -402,6 +402,7 @@ function RA_autoconnect(PrevApi) {
|| (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
|| (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
|| (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
|| (secret_state[SECRET_KEYS.ELECTRONHUB] && oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB)
|| (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
|| (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)
|| (secret_state[SECRET_KEYS.XAI] && oai_settings.chat_completion_source == chat_completion_sources.XAI)
@@ -440,6 +440,7 @@ jQuery(async function () {
'aimlapi': SECRET_KEYS.AIMLAPI,
'moonshot': SECRET_KEYS.MOONSHOT,
'nanogpt': SECRET_KEYS.NANOGPT,
'electronhub': SECRET_KEYS.ELECTRONHUB,
};
if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
@@ -547,6 +548,7 @@ jQuery(async function () {
await processEndpoint('aimlapi', '/api/backends/chat-completions/multimodal-models/aimlapi');
await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations');
await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt');
await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub');
}
await addSettings();
@@ -21,6 +21,7 @@
<option value="anthropic">Anthropic</option>
<option value="cohere">Cohere</option>
<option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
<option value="electronhub">Electron Hub</option>
<option value="google">Google AI Studio</option>
<option value="vertexai">Google Vertex AI</option>
<option value="groq">Groq</option>
+4
View File
@@ -251,6 +251,10 @@ function throwIfInvalidModel(useReverseProxy) {
if (multimodalApi === 'nanogpt' && !secret_state[SECRET_KEYS.NANOGPT]) {
throw new Error('NanoGPT API key is not set.');
}
if (multimodalApi === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB]) {
throw new Error('Electron Hub API key is not set.');
}
}
/**
@@ -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:
@@ -41,6 +41,7 @@
<option value="bfl">BFL (Black Forest Labs)</option>
<option value="comfy">ComfyUI</option>
<option value="drawthings">DrawThings HTTP API</option>
<option value="electronhub">Electron Hub</option>
<option value="extras">Extras API (deprecated)</option>
<option value="falai">FAL.AI</option>
<option value="google">Google AI</option>
@@ -93,6 +94,9 @@
<label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label>
<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="" />
</div>
<div data-sd-source="electronhub">
<i>Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.</i>
</div>
<div data-sd-source="nanogpt">
<i>Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.</i>
</div>
+130 -12
View File
@@ -179,6 +179,7 @@ export const chat_completion_sources = {
COHERE: 'cohere',
PERPLEXITY: 'perplexity',
GROQ: 'groq',
ELECTRONHUB: 'electronhub',
NANOGPT: 'nanogpt',
DEEPSEEK: 'deepseek',
AIMLAPI: 'aimlapi',
@@ -271,6 +272,7 @@ export const settingsToUpdate = {
cohere_model: ['#model_cohere_select', 'cohere_model', false, true],
perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true],
groq_model: ['#model_groq_select', 'groq_model', false, true],
electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true],
nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true],
deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true],
aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true],
@@ -369,6 +371,7 @@ const default_settings = {
cohere_model: 'command-r-plus',
perplexity_model: 'sonar-pro',
groq_model: 'llama-3.3-70b-versatile',
electronhub_model: 'gpt-4o-mini',
nanogpt_model: 'gpt-4o-mini',
deepseek_model: 'deepseek-chat',
aimlapi_model: 'gpt-4o-mini-2024-07-18',
@@ -458,6 +461,7 @@ const oai_settings = {
cohere_model: 'command-r-plus',
perplexity_model: 'sonar-pro',
groq_model: 'llama-3.1-70b-versatile',
electronhub_model: 'gpt-4o-mini',
nanogpt_model: 'gpt-4o-mini',
deepseek_model: 'deepseek-chat',
aimlapi_model: 'gpt-4-turbo',
@@ -1542,6 +1546,11 @@ export function tryParseStreamingError(response, decoded, { quiet = false } = {}
!quiet && toastr.error(data.message, 'Chat Completion API');
throw new Error(data);
}
if (data.detail) {
!quiet && toastr.error(data.detail?.error?.message || response.statusText, 'Chat Completion API');
throw new Error(data);
}
}
catch {
// No JSON. Do nothing.
@@ -1614,6 +1623,8 @@ export function getChatCompletionModel(source = null) {
return oai_settings.perplexity_model;
case chat_completion_sources.GROQ:
return oai_settings.groq_model;
case chat_completion_sources.ELECTRONHUB:
return oai_settings.electronhub_model;
case chat_completion_sources.NANOGPT:
return oai_settings.nanogpt_model;
case chat_completion_sources.DEEPSEEK:
@@ -1774,6 +1785,26 @@ function saveModelList(data) {
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
$('#model_electronhub_select').empty();
model_list.forEach((model) => {
if (model?.endpoints?.includes('/v1/chat/completions')) {
$('#model_electronhub_select').append(
$('<option>', {
value: model.id,
text: model.name,
}));
}
});
const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) {
oai_settings.electronhub_model = model_list[0].id;
}
$('#model_electronhub_select').val(oai_settings.electronhub_model).trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
$('#model_nanogpt_select').empty();
model_list.forEach((model) => {
@@ -2044,24 +2075,43 @@ function getReasoningEffort() {
chat_completion_sources.POLLINATIONS,
chat_completion_sources.PERPLEXITY,
chat_completion_sources.COMETAPI,
chat_completion_sources.ELECTRONHUB,
];
if (!reasoningEffortSources.includes(oai_settings.chat_completion_source)) {
return oai_settings.reasoning_effort;
}
switch (oai_settings.reasoning_effort) {
case reasoning_effort_types.auto:
return undefined;
case reasoning_effort_types.min:
return chat_completion_sources.OPENAI === oai_settings.chat_completion_source && /^gpt-5/.test(oai_settings.openai_model)
? reasoning_effort_types.min
: reasoning_effort_types.low;
case reasoning_effort_types.max:
return reasoning_effort_types.high;
default:
return oai_settings.reasoning_effort;
function resolveReasoningEffort() {
switch (oai_settings.reasoning_effort) {
case reasoning_effort_types.auto:
return undefined;
case reasoning_effort_types.min:
return chat_completion_sources.OPENAI === oai_settings.chat_completion_source && /^gpt-5/.test(oai_settings.openai_model)
? reasoning_effort_types.min
: reasoning_effort_types.low;
case reasoning_effort_types.max:
return reasoning_effort_types.high;
default:
return oai_settings.reasoning_effort;
}
}
const reasoningEffort = resolveReasoningEffort();
// Check if the resolved effort supported by the model
if (oai_settings.chat_completion_source === chat_completion_sources.ELECTRONHUB) {
if (Array.isArray(model_list) && reasoningEffort) {
const currentModel = model_list.find(m => m.id === oai_settings.electronhub_model);
const supportedEfforts = currentModel?.metadata?.supported_reasoning_efforts;
if (Array.isArray(supportedEfforts) && supportedEfforts.includes(reasoningEffort)) {
return reasoningEffort;
}
return undefined;
}
}
return reasoningEffort;
}
/**
@@ -2100,6 +2150,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
const isGroq = oai_settings.chat_completion_source == chat_completion_sources.GROQ;
const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
const isAimlapi = oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI;
const isElectronHub = oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB;
const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;
const isMoonshot = oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT;
@@ -2283,6 +2334,11 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
delete generate_data.max_tokens;
}
// https://docs.electronhub.ai/api-reference/chat/completions
if (isElectronHub) {
generate_data['top_k'] = Number(oai_settings.top_k_openai);
}
const seedSupportedSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.OPENROUTER,
@@ -2290,6 +2346,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
chat_completion_sources.CUSTOM,
chat_completion_sources.COHERE,
chat_completion_sources.GROQ,
chat_completion_sources.ELECTRONHUB,
chat_completion_sources.NANOGPT,
chat_completion_sources.XAI,
chat_completion_sources.POLLINATIONS,
@@ -2462,7 +2519,7 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
}
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
} else if ([chat_completion_sources.CUSTOM, chat_completion_sources.POLLINATIONS, chat_completion_sources.AIMLAPI, chat_completion_sources.MOONSHOT, chat_completion_sources.COMETAPI].includes(chat_completion_source)) {
} 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)) {
if (show_thoughts) {
state.reasoning +=
data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
@@ -3424,6 +3481,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model;
oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model;
oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model;
oai_settings.electronhub_model = settings.electronhub_model ?? default_settings.electronhub_model;
oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;
oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;
oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model;
@@ -3519,6 +3577,8 @@ function loadOpenAISettings(data, settings) {
$(`#model_perplexity_select option[value="${oai_settings.perplexity_model}"`).prop('selected', true);
$('#model_groq_select').val(oai_settings.groq_model);
$(`#model_groq_select option[value="${oai_settings.groq_model}"`).prop('selected', true);
$('#model_electronhub_select').val(oai_settings.electronhub_model);
$(`#model_electronhub_select option[value="${oai_settings.electronhub_model}"`).prop('selected', true);
$('#model_nanogpt_select').val(oai_settings.nanogpt_model);
$(`#model_nanogpt_select option[value="${oai_settings.nanogpt_model}"`).prop('selected', true);
$('#model_deepseek_select').val(oai_settings.deepseek_model);
@@ -3805,6 +3865,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
xai_model: settings.xai_model,
pollinations_model: settings.pollinations_model,
aimlapi_model: settings.aimlapi_model,
electronhub_model: settings.electronhub_model,
moonshot_model: settings.moonshot_model,
fireworks_model: settings.fireworks_model,
cometapi_model: settings.cometapi_model,
@@ -4570,6 +4631,26 @@ function getFireworksMaxContext(model, isUnlocked) {
return max_32k;
}
/**
* Get the maximum context size for the ElectronHub model
* @param {string} model Model identifier
* @param {boolean} isUnlocked Whether context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getElectronHubMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
if (Array.isArray(model_list)) {
const modelInfo = model_list.find(m => m.id === model);
if (modelInfo?.tokens) {
return modelInfo.tokens;
}
}
return max_8k;
}
/**
* Get the maximum context size for the NanoGPT model
* @param {string} model Model identifier
@@ -4681,6 +4762,15 @@ async function onModelChange() {
oai_settings.groq_model = value;
}
if ($(this).is('#model_electronhub_select')) {
if (!value) {
console.debug('Null ElectronHub model selected. Ignoring.');
return;
}
console.log('ElectronHub model changed to', value);
oai_settings.electronhub_model = value;
}
if ($(this).is('#model_nanogpt_select')) {
if (!value) {
console.debug('Null NanoGPT model selected. Ignoring.');
@@ -4922,6 +5012,15 @@ async function onModelChange() {
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
const maxContext = getElectronHubMaxContext(oai_settings.electronhub_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) {
const maxContext = getNanoGptMaxContext(oai_settings.nanogpt_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
@@ -5216,6 +5315,19 @@ async function onConnectButtonClick(e) {
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
const api_key_electronhub = String($('#api_key_electronhub').val()).trim();
if (api_key_electronhub.length) {
await writeSecret(SECRET_KEYS.ELECTRONHUB, api_key_electronhub);
}
if (!secret_state[SECRET_KEYS.ELECTRONHUB]) {
console.log('No secret key saved for Electron Hub');
return;
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
const api_key_nanogpt = String($('#api_key_nanogpt').val()).trim();
@@ -5350,6 +5462,9 @@ function toggleChatCompletionForms() {
else if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
$('#model_groq_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
$('#model_electronhub_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
$('#model_nanogpt_select').trigger('change');
}
@@ -5518,6 +5633,8 @@ export function isImageInliningSupported() {
return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
case chat_completion_sources.AIMLAPI:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.aimlapi_model)?.features?.includes('openai/chat-completion.vision'));
case chat_completion_sources.ELECTRONHUB:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.electronhub_model)?.metadata?.vision);
case chat_completion_sources.POLLINATIONS:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.vision);
case chat_completion_sources.COMETAPI:
@@ -6326,6 +6443,7 @@ export function initOpenAI() {
$('#model_cohere_select').on('change', onModelChange);
$('#model_perplexity_select').on('change', onModelChange);
$('#model_groq_select').on('change', onModelChange);
$('#model_electronhub_select').on('change', onModelChange);
$('#model_nanogpt_select').on('change', onModelChange);
$('#model_deepseek_select').on('change', onModelChange);
$('#model_aimlapi_select').on('change', onModelChange);
+3
View File
@@ -51,6 +51,7 @@ export const SECRET_KEYS = {
HUGGINGFACE: 'api_key_huggingface',
STABILITY: 'api_key_stability',
CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
ELECTRONHUB: 'api_key_electronhub',
NANOGPT: 'api_key_nanogpt',
TAVILY: 'api_key_tavily',
BFL: 'api_key_bfl',
@@ -95,6 +96,7 @@ const FRIENDLY_NAMES = {
[SECRET_KEYS.GROQ]: 'Groq',
[SECRET_KEYS.FEATHERLESS]: 'Featherless',
[SECRET_KEYS.HUGGINGFACE]: 'HuggingFace',
[SECRET_KEYS.ELECTRONHUB]: 'Electron Hub',
[SECRET_KEYS.NANOGPT]: 'NanoGPT',
[SECRET_KEYS.GENERIC]: 'Generic (OpenAI-compatible)',
[SECRET_KEYS.DEEPSEEK]: 'DeepSeek',
@@ -148,6 +150,7 @@ const INPUT_MAP = {
[SECRET_KEYS.GROQ]: '#api_key_groq',
[SECRET_KEYS.FEATHERLESS]: '#api_key_featherless',
[SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface',
[SECRET_KEYS.ELECTRONHUB]: '#api_key_electronhub',
[SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
[SECRET_KEYS.GENERIC]: '#api_key_generic',
[SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',
+1
View File
@@ -4841,6 +4841,7 @@ function getModelOptions(quiet) {
{ id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
{ id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
{ id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
{ id: 'model_electronhub_select', api: 'openai', type: chat_completion_sources.ELECTRONHUB },
{ id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
{ id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
{ id: 'model_aimlapi_select', api: 'openai', type: chat_completion_sources.AIMLAPI },
+8
View File
@@ -633,6 +633,13 @@ export class ToolManager {
}
}
if (oai_settings.chat_completion_source === chat_completion_sources.ELECTRONHUB && Array.isArray(model_list)) {
const currentModel = model_list.find(model => model.id === oai_settings.electronhub_model);
if (currentModel && currentModel.metadata?.function_call) {
return currentModel.metadata.function_call;
}
}
const supportedSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.CUSTOM,
@@ -651,6 +658,7 @@ export class ToolManager {
chat_completion_sources.MOONSHOT,
chat_completion_sources.FIREWORKS,
chat_completion_sources.COMETAPI,
chat_completion_sources.ELECTRONHUB,
];
return supportedSources.includes(oai_settings.chat_completion_source);
}
+1
View File
@@ -173,6 +173,7 @@ export const CHAT_COMPLETION_SOURCES = {
COHERE: 'cohere',
PERPLEXITY: 'perplexity',
GROQ: 'groq',
ELECTRONHUB: 'electronhub',
NANOGPT: 'nanogpt',
DEEPSEEK: 'deepseek',
AIMLAPI: 'aimlapi',
+124
View File
@@ -61,6 +61,7 @@ const API_GROQ = 'https://api.groq.com/openai/v1';
const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com';
const API_AI21 = 'https://api.ai21.com/studio/v1';
const API_ELECTRONHUB = 'https://api.electronhub.ai/v1';
const API_NANOGPT = 'https://nano-gpt.com/api/v1';
const API_DEEPSEEK = 'https://api.deepseek.com/beta';
const API_XAI = 'https://api.x.ai/v1';
@@ -1193,6 +1194,106 @@ async function sendAimlapiRequest(request, response) {
}
}
/**
* Sends a request to Electron Hub.
* @param {express.Request} request Express request
* @param {express.Response} response Express response
*/
async function sendElectronHubRequest(request, response) {
const apiUrl = API_ELECTRONHUB;
const apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
if (!apiKey) {
console.warn('Electron Hub key is missing.');
return response.status(400).send({ error: true });
}
const controller = new AbortController();
request.socket.removeAllListeners('close');
request.socket.on('close', function () {
controller.abort();
});
try {
let bodyParams = {};
if (request.body.enable_web_search) {
bodyParams['web_search'] = true;
}
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
bodyParams['tools'] = request.body.tools;
bodyParams['tool_choice'] = request.body.tool_choice;
}
if (request.body.reasoning_effort) {
bodyParams['reasoning_effort'] = request.body.reasoning_effort;
}
if (request.body.json_schema) {
bodyParams['response_format'] = {
type: 'json_schema',
json_schema: {
name: request.body.json_schema.name,
description: request.body.json_schema.description,
schema: request.body.json_schema.value,
strict: request.body.json_schema.strict ?? true,
},
};
}
const requestBody = {
'messages': request.body.messages,
'model': request.body.model,
'temperature': request.body.temperature,
'max_tokens': request.body.max_tokens,
'stream': request.body.stream,
'presence_penalty': request.body.presence_penalty,
'frequency_penalty': request.body.frequency_penalty,
'top_p': request.body.top_p,
'top_k': request.body.top_k,
'seed': request.body.seed,
...bodyParams,
};
const config = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey,
},
body: JSON.stringify(requestBody),
signal: controller.signal,
};
console.debug('Electron Hub request:', requestBody);
const generateResponse = await fetch(apiUrl + '/chat/completions', config);
if (request.body.stream) {
forwardFetchResponse(generateResponse, response);
} else {
if (!generateResponse.ok) {
const errorText = await generateResponse.text();
console.warn('Electron Hub returned error: ', errorText);
const errorJson = tryParse(errorText) ?? { error: true };
return response.status(500).send(errorJson);
}
const generateResponseJson = await generateResponse.json();
console.debug('Electron Hub response:', generateResponseJson);
return response.send(generateResponseJson);
}
}
catch (error) {
console.error('Error communicating with Electron Hub: ', error);
if (!response.headersSent) {
response.send({ error: true });
} else {
response.end();
}
}
}
export const router = express.Router();
router.post('/status', async function (request, statusResponse) {
@@ -1225,6 +1326,10 @@ router.post('/status', async function (request, statusResponse) {
apiUrl = API_COHERE_V1;
apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ELECTRONHUB) {
apiUrl = API_ELECTRONHUB;
apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) {
apiUrl = API_NANOGPT;
apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
@@ -1490,6 +1595,7 @@ router.post('/generate', function (request, response) {
case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response);
case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return sendElectronHubRequest(request, response);
}
let apiUrl;
@@ -1927,4 +2033,22 @@ multimodalModels.post('/nanogpt', async (_req, res) => {
}
});
multimodalModels.post('/electronhub', async (_req, res) => {
try {
const response = await fetch('https://api.electronhub.ai/v1/models');
if (!response.ok) {
return res.json([]);
}
/** @type {any} */
const data = await response.json();
const multimodalModels = data.data.filter(m => m.metadata?.vision).map(m => m.id);
return res.json(multimodalModels);
} catch (error) {
console.error(error);
return res.sendStatus(500);
}
});
router.use('/multimodal-models', multimodalModels);
+8
View File
@@ -81,6 +81,10 @@ router.post('/caption-image', async (request, response) => {
key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
}
if (request.body.api === 'electronhub') {
key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
}
const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp', 'pollinations'];
if (!key && !request.body.reverse_proxy && !noKeyTypes.includes(request.body.api)) {
console.warn('No key found for API', request.body.api);
@@ -169,6 +173,10 @@ router.post('/caption-image', async (request, response) => {
apiUrl = 'https://nano-gpt.com/api/v1/chat/completions';
}
if (request.body.api === 'electronhub') {
apiUrl = 'https://api.electronhub.ai/v1/chat/completions';
}
if (['koboldcpp', 'vllm', 'llamacpp', 'ooba'].includes(request.body.api)) {
apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;
}
+1
View File
@@ -45,6 +45,7 @@ export const SECRET_KEYS = {
STABILITY: 'api_key_stability',
CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
TAVILY: 'api_key_tavily',
ELECTRONHUB: 'api_key_electronhub',
NANOGPT: 'api_key_nanogpt',
BFL: 'api_key_bfl',
FALAI: 'api_key_falai',
+113
View File
@@ -957,6 +957,118 @@ huggingface.post('/generate', async (request, response) => {
}
});
const electronhub = express.Router();
electronhub.post('/models', async (request, response) => {
try {
const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
if (!key) {
console.warn('Electron Hub key not found.');
return response.sendStatus(400);
}
const modelsResponse = await fetch('https://api.electronhub.ai/v1/models', {
method: 'GET',
headers: {
'Authorization': `Bearer ${key}`,
'Content-Type': 'application/json',
},
});
if (!modelsResponse.ok) {
console.warn('Electron Hub returned an error.');
return response.sendStatus(500);
}
/** @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 }));
return response.send(models);
} catch (error) {
console.error(error);
return response.sendStatus(500);
}
});
electronhub.post('/generate', async (request, response) => {
try {
const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
if (!key) {
console.warn('Electron Hub key not found.');
return response.sendStatus(400);
}
let bodyParams = {
model: request.body.model,
prompt: request.body.prompt,
response_format: 'b64_json',
};
if (request.body.size) {
bodyParams.size = request.body.size;
}
const result = await fetch('https://api.electronhub.ai/v1/images/generations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${key}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
...bodyParams,
}),
});
if (!result.ok) {
const errorText = await result.text();
console.warn('Electron Hub returned an error.', result.status, result.statusText, errorText);
return response.sendStatus(500);
}
/** @type {any} */
const data = await result.json();
const image = data?.data?.[0]?.b64_json;
if (!image) {
console.warn('Electron Hub returned invalid data.');
return response.sendStatus(500);
}
return response.send({ image });
} catch (error) {
console.error(error);
return response.sendStatus(500);
}
});
electronhub.post('/sizes', async (request, response) => {
const result = await fetch(`https://api.electronhub.ai/v1/models/${request.body.model}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!result.ok) {
console.warn('Electron Hub returned an error.');
return response.sendStatus(500);
}
/** @type {any} */
const data = await result.json();
const sizes = data.sizes;
if (!sizes) {
console.warn('Electron Hub returned invalid data.');
return response.sendStatus(500);
}
return response.send({ sizes });
});
const nanogpt = express.Router();
nanogpt.post('/models', async (request, response) => {
@@ -1439,6 +1551,7 @@ router.use('/drawthings', drawthings);
router.use('/pollinations', pollinations);
router.use('/stability', stability);
router.use('/huggingface', huggingface);
router.use('/electronhub', electronhub);
router.use('/nanogpt', nanogpt);
router.use('/bfl', bfl);
router.use('/falai', falai);