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>
Signed| @@ -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 | |
| @@ -691,7 +691,7 @@ | ||
| 691 | 691 | </span> |
| 692 | 692 | </div> |
| 693 | 693 | </div> |
| 694 | 694 | <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"> |
| 695 | 695 | <div class="range-block-title" data-i18n="Temperature"> |
| 696 | 696 | Temperature |
| 697 | 697 | </div> |
| @@ -704,7 +704,7 @@ | ||
| 704 | 704 | </div> |
| 705 | 705 | </div> |
| 706 | 706 | </div> |
| 707 | 707 | <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi"> |
| 708 | 708 | <div class="range-block-title" data-i18n="Frequency Penalty"> |
| 709 | 709 | Frequency Penalty |
| 710 | 710 | </div> |
| @@ -717,7 +717,7 @@ | ||
| 717 | 717 | </div> |
| 718 | 718 | </div> |
| 719 | 719 | </div> |
| 720 | 720 | <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi"> |
| 721 | 721 | <div class="range-block-title" data-i18n="Presence Penalty"> |
| 722 | 722 | Presence Penalty |
| 723 | 723 | </div> |
| @@ -730,7 +730,7 @@ | ||
| 730 | 730 | </div> |
| 731 | 731 | </div> |
| 732 | 732 | </div> |
| 733 | 733 | <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub"> |
| 734 | 734 | <div class="range-block-title" data-i18n="Top K"> |
| 735 | 735 | Top K |
| 736 | 736 | </div> |
| @@ -743,7 +743,7 @@ | ||
| 743 | 743 | </div> |
| 744 | 744 | </div> |
| 745 | 745 | </div> |
| 746 | 746 | <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"> |
| 747 | 747 | <div class="range-block-title" data-i18n="Top P"> |
| 748 | 748 | Top P |
| 749 | 749 | </div> |
| @@ -980,7 +980,7 @@ | ||
| 980 | 980 | </div> |
| 981 | 981 | </div> |
| 982 | 982 | </div> |
| 983 | 983 | <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai"> |
| 984 | 984 | <div class="range-block-title justifyLeft" data-i18n="Seed"> |
| 985 | 985 | Seed |
| 986 | 986 | </div> |
| @@ -1970,7 +1970,7 @@ | ||
| 1970 | 1970 | </span> |
| 1971 | 1971 | </div> |
| 1972 | 1972 | </div> |
| 1973 | 1973 | <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,electronhub,nanogpt"> |
| 1974 | 1974 | <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand"> |
| 1975 | 1975 | <input id="openai_enable_web_search" type="checkbox" /> |
| 1976 | 1976 | <span data-i18n="Enable web search">Enable web search</span> |
| @@ -1984,7 +1984,7 @@ | ||
| 1984 | 1984 | </b> |
| 1985 | 1985 | </div> |
| 1986 | 1986 | </div> |
| 1987 | 1987 | <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub"> |
| 1988 | 1988 | <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand"> |
| 1989 | 1989 | <input id="openai_function_calling" type="checkbox" /> |
| 1990 | 1990 | <span data-i18n="Enable function calling">Enable function calling</span> |
| @@ -1999,7 +1999,7 @@ | ||
| 1999 | 1999 | <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong> |
| 2000 | 2000 | </div> |
| 2001 | 2001 | </div> |
| 2002 | 2002 | <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub"> |
| 2003 | 2003 | <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand"> |
| 2004 | 2004 | <input id="openai_image_inlining" type="checkbox" /> |
| 2005 | 2005 | <span data-i18n="Send inline images">Send inline images</span> |
| @@ -2015,7 +2015,7 @@ | ||
| 2015 | 2015 | <code><i class="fa-solid fa-wand-magic-sparkles"></i></code> |
| 2016 | 2016 | <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span> |
| 2017 | 2017 | </div> |
| 2018 | 2018 | <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub"> |
| 2019 | 2019 | <div class="flex-container oneline-dropdown"> |
| 2020 | 2020 | <label for="openai_inline_image_quality" data-i18n="Inline Image Quality"> |
| 2021 | 2021 | Inline Image Quality |
| @@ -2077,7 +2077,7 @@ | ||
| 2077 | 2077 | </span> |
| 2078 | 2078 | </div> |
| 2079 | 2079 | </div> |
| 2080 | 2080 | <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub"> |
| 2081 | 2081 | <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand"> |
| 2082 | 2082 | <input id="openai_show_thoughts" type="checkbox" /> |
| 2083 | 2083 | <span data-i18n="Request model reasoning">Request model reasoning</span> |
| @@ -2091,7 +2091,7 @@ | ||
| 2091 | 2091 | </span> |
| 2092 | 2092 | </div> |
| 2093 | 2093 | </div> |
| 2094 | 2094 | <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub"> |
| 2095 | 2095 | <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models. 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."> |
| 2096 | 2096 | <label for="openai_reasoning_effort"> |
| 2097 | 2097 | <span data-i18n="Reasoning Effort">Reasoning Effort</span> |
| @@ -2105,7 +2105,7 @@ | ||
| 2105 | 2105 | <option data-i18n="openai_reasoning_effort_high" value="high">High</option> |
| 2106 | 2106 | <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option> |
| 2107 | 2107 | </select> |
| 2108 | 2108 | <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."> |
| 2109 | 2109 | OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level. |
| 2110 | 2110 | </div> |
| 2111 | 2111 | <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 @@ | ||
| 2807 | 2807 | <!-- Temporarily disabled. --> |
| 2808 | 2808 | <!-- <option value="cometapi">CometAPI</option> --> |
| 2809 | 2809 | <option value="deepseek">DeepSeek</option> |
| 2810 | + <option value="electronhub">Electron Hub</option> | |
| 2810 | 2811 | <option value="fireworks">Fireworks AI</option> |
| 2811 | 2812 | <option value="groq">Groq</option> |
| 2812 | 2813 | <option value="makersuite">Google AI Studio</option> |
| @@ -3469,6 +3470,20 @@ | ||
| 3469 | 3470 | <option value="mistral-saba-24b">mistral-saba-24b</option> |
| 3470 | 3471 | </select> |
| 3471 | 3472 | </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> | |
| 3472 | 3487 | <div id="nanogpt_form" data-source="nanogpt"> |
| 3473 | 3488 | <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4> |
| 3474 | 3489 | <div class="flex-container"> |
| @@ -317,6 +317,8 @@ | ||
| 317 | 317 | "flag": "وضع علامة", |
| 318 | 318 | "API key (optional)": "مفتاح API (اختياري)", |
| 319 | 319 | "Server url": "رابط الخادم", |
| 320 | + "Electron Hub API Key": "مفتاح API لـ Electron Hub", | |
| 321 | + "Electron Hub Model": "نموذج Electron Hub", | |
| 320 | 322 | "Example: http://127.0.0.1:5000": "مثال: http://127.0.0.1:5000", |
| 321 | 323 | "Custom model (optional)": "نموذج مخصص (اختياري)", |
| 322 | 324 | "vllm-project/vllm": "vllm-project/vllm (وضع غلاف OpenAI API)", |
| @@ -317,6 +317,8 @@ | ||
| 317 | 317 | "flag": "Flagge", |
| 318 | 318 | "API key (optional)": "API-Schlüssel (optional)", |
| 319 | 319 | "Server url": "Server-URL", |
| 320 | + "Electron Hub API Key": "Electron Hub API-Schlüssel", | |
| 321 | + "Electron Hub Model": "Electron Hub-Modell", | |
| 320 | 322 | "Example: http://127.0.0.1:5000": "Beispiel: http://127.0.0.1:5000", |
| 321 | 323 | "Custom model (optional)": "Benutzerdefiniertes Modell (optional)", |
| 322 | 324 | "vllm-project/vllm": "vllm-project/vllm (OpenAI API-Wrappermodus)", |
| @@ -317,6 +317,8 @@ | ||
| 317 | 317 | "flag": "bandera", |
| 318 | 318 | "API key (optional)": "Clave API (opcional)", |
| 319 | 319 | "Server url": "URL del servidor", |
| 320 | + "Electron Hub API Key": "Clave API de Electron Hub", | |
| 321 | + "Electron Hub Model": "Modelo de Electron Hub", | |
| 320 | 322 | "Example: http://127.0.0.1:5000": "Ejemplo: http://127.0.0.1:5000", |
| 321 | 323 | "Custom model (optional)": "Modelo personalizado (opcional)", |
| 322 | 324 | "vllm-project/vllm": "vllm-project/vllm (modo contenedor de API OpenAI)", |
| @@ -1411,6 +1411,8 @@ | ||
| 1411 | 1411 | "Do not proceed if you do not agree to this!": "Ne continuez pas si vous n'êtes pas d'accord avec cela !", |
| 1412 | 1412 | "Claude API Key": "Clé API Claude", |
| 1413 | 1413 | "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", | |
| 1414 | 1416 | "NanoGPT API Key": "Clé API NanoGPT", |
| 1415 | 1417 | "NanoGPT Model": "Modèle NanoGPT", |
| 1416 | 1418 | "DeepSeek API Key": "Clé API DeepSeek", |
| @@ -317,6 +317,8 @@ | ||
| 317 | 317 | "flag": "bandiera", |
| 318 | 318 | "API key (optional)": "Chiave API (opzionale)", |
| 319 | 319 | "Server url": "URL del server", |
| 320 | + "Electron Hub API Key": "Chiave API di Electron Hub", | |
| 321 | + "Electron Hub Model": "Modello di Electron Hub", | |
| 320 | 322 | "Example: http://127.0.0.1:5000": "Esempio: http://127.0.0.1:5000", |
| 321 | 323 | "Custom model (optional)": "Modello personalizzato (opzionale)", |
| 322 | 324 | "vllm-project/vllm": "vllm-project/vllm (modalità wrapper API OpenAI)", |
| @@ -317,6 +317,8 @@ | ||
| 317 | 317 | "flag": "フラグ", |
| 318 | 318 | "API key (optional)": "APIキー(オプション)", |
| 319 | 319 | "Server url": "サーバーURL", |
| 320 | + "Electron Hub API Key": "Electron Hub API キー", | |
| 321 | + "Electron Hub Model": "Electron Hub モデル", | |
| 320 | 322 | "Example: http://127.0.0.1:5000": "例: http://127.0.0.1:5000", |
| 321 | 323 | "Custom model (optional)": "カスタムモデル(オプション)", |
| 322 | 324 | "vllm-project/vllm": "vllm-project/vllm (OpenAI API ラッパーモード)", |
| @@ -319,6 +319,8 @@ | ||
| 319 | 319 | "flag": "깃발", |
| 320 | 320 | "API key (optional)": "API 키 (선택 사항)", |
| 321 | 321 | "Server url": "서버 URL", |
| 322 | + "Electron Hub API Key": "Electron Hub API 키", | |
| 323 | + "Electron Hub Model": "Electron Hub 모델", | |
| 322 | 324 | "Example: http://127.0.0.1:5000": "예시: http://127.0.0.1:5000", |
| 323 | 325 | "Custom model (optional)": "사용자 정의 모델 (선택 사항)", |
| 324 | 326 | "vllm-project/vllm": "vllm-project/vllm(OpenAI API 래퍼 모드)", |
| @@ -317,6 +317,8 @@ | ||
| 317 | 317 | "flag": "bandeira", |
| 318 | 318 | "API key (optional)": "Chave da API (opcional)", |
| 319 | 319 | "Server url": "URL do servidor", |
| 320 | + "Electron Hub API Key": "Chave API Electron Hub", | |
| 321 | + "Electron Hub Model": "Modelo Electron Hub", | |
| 320 | 322 | "Example: http://127.0.0.1:5000": "Exemplo: http://127.0.0.1:5000", |
| 321 | 323 | "Custom model (optional)": "Modelo personalizado (opcional)", |
| 322 | 324 | "vllm-project/vllm": "vllm-project/vllm (modo wrapper da API OpenAI)", |
| @@ -1994,6 +1994,8 @@ | ||
| 1994 | 1994 | "Click on the setting name to omit it from the profile.": "Нажмите на название настройки, чтобы исключить её из профиля", |
| 1995 | 1995 | "Included settings:": "Сохранённые параметры:", |
| 1996 | 1996 | "Server URL": "Адрес сервера", |
| 1997 | + "Electron Hub API Key": "Ключ от API Electron Hub", | |
| 1998 | + "Electron Hub Model": "Модель Electron Hub", | |
| 1997 | 1999 | "NanoGPT API Key": "Ключ от API NanoGPT", |
| 1998 | 2000 | "NanoGPT Model": "Модель NanoGPT", |
| 1999 | 2001 | "Use extension settings": "Использовать настройки из расширения", |
| @@ -317,6 +317,8 @@ | ||
| 317 | 317 | "flag": "прапорцем", |
| 318 | 318 | "API key (optional)": "Ключ API (необов'язково)", |
| 319 | 319 | "Server url": "URL-адреса сервера", |
| 320 | + "Electron Hub API Key": "Ключ API для Electron Hub", | |
| 321 | + "Electron Hub Model": "Модель Electron Hub", | |
| 320 | 322 | "Example: http://127.0.0.1:5000": "Приклад: http://127.0.0.1:5000", |
| 321 | 323 | "Custom model (optional)": "Власна модель (необов'язково)", |
| 322 | 324 | "vllm-project/vllm": "vllm-project/vllm (режим оболонки OpenAI API)", |
| @@ -317,6 +317,8 @@ | ||
| 317 | 317 | "flag": "cờ", |
| 318 | 318 | "API key (optional)": "Key API (tùy chọn)", |
| 319 | 319 | "Server url": "URL máy chủ", |
| 320 | + "Electron Hub API Key": "Key API Electron Hub", | |
| 321 | + "Electron Hub Model": "Model Electron Hub", | |
| 320 | 322 | "Example: http://127.0.0.1:5000": "Ví dụ: http://127.0.0.1:5000", |
| 321 | 323 | "Custom model (optional)": "Model tùy chỉnh (tùy chọn)", |
| 322 | 324 | "vllm-project/vllm": "vllm-project/vllm (Chế độ trình bao bọc API OpenAI)", |
| @@ -481,6 +481,8 @@ | ||
| 481 | 481 | "MistralAI Model": "MistralAI 模型", |
| 482 | 482 | "Groq API Key": "Groq API 密钥", |
| 483 | 483 | "Groq Model": "Groq 模型", |
| 484 | + "Electron Hub API Key": "Electron Hub API 密钥", | |
| 485 | + "Electron Hub Model": "Electron Hub 模型", | |
| 484 | 486 | "NanoGPT API Key": "NanoGPT API 密钥", |
| 485 | 487 | "NanoGPT Model": "NanoGPT 模型", |
| 486 | 488 | "DeepSeek API Key": "DeepSeek API 密钥", |
| @@ -1791,6 +1791,8 @@ | ||
| 1791 | 1791 | "Derive context size from backend": "從後端推導上下文大小", |
| 1792 | 1792 | "Using a proxy that you're not running yourself is a risk to your data privacy.": "使用非自行管理的代理服務可能導致您的資料隱私外洩。", |
| 1793 | 1793 | "Claude API Key": "Claude API 金鑰", |
| 1794 | + "Electron Hub API Key": "Electron Hub API 金鑰", | |
| 1795 | + "Electron Hub Model": "Electron Hub 模型", | |
| 1794 | 1796 | "NanoGPT API Key": "NanoGPT API 金鑰", |
| 1795 | 1797 | "NanoGPT Model": "NanoGPT 模型", |
| 1796 | 1798 | "context_derived": "若可能,根據模型後設資料推導。", |
| @@ -5369,6 +5369,7 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource | ||
| 5369 | 5369 | case chat_completion_sources.CUSTOM: |
| 5370 | 5370 | case chat_completion_sources.COHERE: |
| 5371 | 5371 | case chat_completion_sources.XAI: |
| 5372 | + case chat_completion_sources.ELECTRONHUB: | |
| 5372 | 5373 | default: |
| 5373 | 5374 | result = tryParse(text); |
| 5374 | 5375 | break; |
| @@ -402,6 +402,7 @@ function RA_autoconnect(PrevApi) { | ||
| 402 | 402 | || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE) |
| 403 | 403 | || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY) |
| 404 | 404 | || (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) | |
| 405 | 406 | || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) |
| 406 | 407 | || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) |
| 407 | 408 | || (secret_state[SECRET_KEYS.XAI] && oai_settings.chat_completion_source == chat_completion_sources.XAI) |
| @@ -440,6 +440,7 @@ jQuery(async function () { | ||
| 440 | 440 | 'aimlapi': SECRET_KEYS.AIMLAPI, |
| 441 | 441 | 'moonshot': SECRET_KEYS.MOONSHOT, |
| 442 | 442 | 'nanogpt': SECRET_KEYS.NANOGPT, |
| 443 | + 'electronhub': SECRET_KEYS.ELECTRONHUB, | |
| 443 | 444 | }; |
| 444 | 445 | |
| 445 | 446 | if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) { |
| @@ -547,6 +548,7 @@ jQuery(async function () { | ||
| 547 | 548 | await processEndpoint('aimlapi', '/api/backends/chat-completions/multimodal-models/aimlapi'); |
| 548 | 549 | await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations'); |
| 549 | 550 | await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt'); |
| 551 | + await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub'); | |
| 550 | 552 | } |
| 551 | 553 | |
| 552 | 554 | await addSettings(); |
| @@ -21,6 +21,7 @@ | ||
| 21 | 21 | <option value="anthropic">Anthropic</option> |
| 22 | 22 | <option value="cohere">Cohere</option> |
| 23 | 23 | <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option> |
| 24 | + <option value="electronhub">Electron Hub</option> | |
| 24 | 25 | <option value="google">Google AI Studio</option> |
| 25 | 26 | <option value="vertexai">Google Vertex AI</option> |
| 26 | 27 | <option value="groq">Groq</option> |
| @@ -251,6 +251,10 @@ function throwIfInvalidModel(useReverseProxy) { | ||
| 251 | 251 | if (multimodalApi === 'nanogpt' && !secret_state[SECRET_KEYS.NANOGPT]) { |
| 252 | 252 | throw new Error('NanoGPT API key is not set.'); |
| 253 | 253 | } |
| 254 | + | |
| 255 | + if (multimodalApi === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB]) { | |
| 256 | + throw new Error('Electron Hub API key is not set.'); | |
| 257 | + } | |
| 254 | 258 | } |
| 255 | 259 | |
| 256 | 260 | /** |
| @@ -82,6 +82,7 @@ const sources = { | ||
| 82 | 82 | pollinations: 'pollinations', |
| 83 | 83 | stability: 'stability', |
| 84 | 84 | huggingface: 'huggingface', |
| 85 | + electronhub: 'electronhub', | |
| 85 | 86 | nanogpt: 'nanogpt', |
| 86 | 87 | bfl: 'bfl', |
| 87 | 88 | falai: 'falai', |
| @@ -1289,6 +1290,7 @@ async function onModelChange() { | ||
| 1289 | 1290 | sources.pollinations, |
| 1290 | 1291 | sources.stability, |
| 1291 | 1292 | sources.huggingface, |
| 1293 | + sources.electronhub, | |
| 1292 | 1294 | sources.nanogpt, |
| 1293 | 1295 | sources.bfl, |
| 1294 | 1296 | sources.falai, |
| @@ -1506,6 +1508,9 @@ async function loadSamplers() { | ||
| 1506 | 1508 | case sources.huggingface: |
| 1507 | 1509 | samplers = ['N/A']; |
| 1508 | 1510 | break; |
| 1511 | + case sources.electronhub: | |
| 1512 | + samplers = ['N/A']; | |
| 1513 | + break; | |
| 1509 | 1514 | case sources.nanogpt: |
| 1510 | 1515 | samplers = ['N/A']; |
| 1511 | 1516 | break; |
| @@ -1702,6 +1707,9 @@ async function loadModels() { | ||
| 1702 | 1707 | case sources.huggingface: |
| 1703 | 1708 | models = [{ value: '', text: '<Enter Model ID above>' }]; |
| 1704 | 1709 | break; |
| 1710 | + case sources.electronhub: | |
| 1711 | + models = await loadElectronHubModels(); | |
| 1712 | + break; | |
| 1705 | 1713 | case sources.nanogpt: |
| 1706 | 1714 | models = await loadNanoGPTModels(); |
| 1707 | 1715 | break; |
| @@ -1806,6 +1814,24 @@ async function loadTogetherAIModels() { | ||
| 1806 | 1814 | return []; |
| 1807 | 1815 | } |
| 1808 | 1816 | |
| 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 | + | |
| 1809 | 1835 | async function loadNanoGPTModels() { |
| 1810 | 1836 | if (!secret_state[SECRET_KEYS.NANOGPT]) { |
| 1811 | 1837 | console.debug('NanoGPT API key is not set.'); |
| @@ -2131,6 +2157,9 @@ async function loadSchedulers() { | ||
| 2131 | 2157 | case sources.huggingface: |
| 2132 | 2158 | schedulers = ['N/A']; |
| 2133 | 2159 | break; |
| 2160 | + case sources.electronhub: | |
| 2161 | + schedulers = ['N/A']; | |
| 2162 | + break; | |
| 2134 | 2163 | case sources.nanogpt: |
| 2135 | 2164 | schedulers = ['N/A']; |
| 2136 | 2165 | break; |
| @@ -2228,6 +2257,9 @@ async function loadVaes() { | ||
| 2228 | 2257 | case sources.huggingface: |
| 2229 | 2258 | vaes = ['N/A']; |
| 2230 | 2259 | break; |
| 2260 | + case sources.electronhub: | |
| 2261 | + vaes = ['N/A']; | |
| 2262 | + break; | |
| 2231 | 2263 | case sources.nanogpt: |
| 2232 | 2264 | vaes = ['N/A']; |
| 2233 | 2265 | break; |
| @@ -2811,6 +2843,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2811 | 2843 | case sources.huggingface: |
| 2812 | 2844 | result = await generateHuggingFaceImage(prefixedPrompt, signal); |
| 2813 | 2845 | break; |
| 2846 | + case sources.electronhub: | |
| 2847 | + result = await generateElectronHubImage(prefixedPrompt, signal); | |
| 2848 | + break; | |
| 2814 | 2849 | case sources.nanogpt: |
| 2815 | 2850 | result = await generateNanoGPTImage(prefixedPrompt, negativePrompt, signal); |
| 2816 | 2851 | break; |
| @@ -3014,6 +3049,56 @@ function getClosestAspectRatio(width, height, source) { | ||
| 3014 | 3049 | } |
| 3015 | 3050 | |
| 3016 | 3051 | /** |
| 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 | +/** | |
| 3017 | 3102 | * Generates an image using Stability AI. |
| 3018 | 3103 | * @param {string} prompt - The main instruction used to guide the image generation. |
| 3019 | 3104 | * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| @@ -3565,6 +3650,35 @@ async function generateHuggingFaceImage(prompt, signal) { | ||
| 3565 | 3650 | } |
| 3566 | 3651 | |
| 3567 | 3652 | /** |
| 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 | +/** | |
| 3568 | 3682 | * Generates an image using the NanoGPT API. |
| 3569 | 3683 | * @param {string} prompt - The main instruction used to guide the image generation. |
| 3570 | 3684 | * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| @@ -4014,6 +4128,8 @@ function isValidState() { | ||
| 4014 | 4128 | return secret_state[SECRET_KEYS.STABILITY]; |
| 4015 | 4129 | case sources.huggingface: |
| 4016 | 4130 | return secret_state[SECRET_KEYS.HUGGINGFACE]; |
| 4131 | + case sources.electronhub: | |
| 4132 | + return secret_state[SECRET_KEYS.ELECTRONHUB]; | |
| 4017 | 4133 | case sources.nanogpt: |
| 4018 | 4134 | return secret_state[SECRET_KEYS.NANOGPT]; |
| 4019 | 4135 | case sources.bfl: |
| @@ -41,6 +41,7 @@ | ||
| 41 | 41 | <option value="bfl">BFL (Black Forest Labs)</option> |
| 42 | 42 | <option value="comfy">ComfyUI</option> |
| 43 | 43 | <option value="drawthings">DrawThings HTTP API</option> |
| 44 | + <option value="electronhub">Electron Hub</option> | |
| 44 | 45 | <option value="extras">Extras API (deprecated)</option> |
| 45 | 46 | <option value="falai">FAL.AI</option> |
| 46 | 47 | <option value="google">Google AI</option> |
| @@ -93,6 +94,9 @@ | ||
| 93 | 94 | <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label> |
| 94 | 95 | <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="" /> |
| 95 | 96 | </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> | |
| 96 | 100 | <div data-sd-source="nanogpt"> |
| 97 | 101 | <i>Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.</i> |
| 98 | 102 | </div> |
| @@ -179,6 +179,7 @@ export const chat_completion_sources = { | ||
| 179 | 179 | COHERE: 'cohere', |
| 180 | 180 | PERPLEXITY: 'perplexity', |
| 181 | 181 | GROQ: 'groq', |
| 182 | + ELECTRONHUB: 'electronhub', | |
| 182 | 183 | NANOGPT: 'nanogpt', |
| 183 | 184 | DEEPSEEK: 'deepseek', |
| 184 | 185 | AIMLAPI: 'aimlapi', |
| @@ -271,6 +272,7 @@ export const settingsToUpdate = { | ||
| 271 | 272 | cohere_model: ['#model_cohere_select', 'cohere_model', false, true], |
| 272 | 273 | perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true], |
| 273 | 274 | groq_model: ['#model_groq_select', 'groq_model', false, true], |
| 275 | + electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], | |
| 274 | 276 | nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], |
| 275 | 277 | deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], |
| 276 | 278 | aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], |
| @@ -369,6 +371,7 @@ const default_settings = { | ||
| 369 | 371 | cohere_model: 'command-r-plus', |
| 370 | 372 | perplexity_model: 'sonar-pro', |
| 371 | 373 | groq_model: 'llama-3.3-70b-versatile', |
| 374 | + electronhub_model: 'gpt-4o-mini', | |
| 372 | 375 | nanogpt_model: 'gpt-4o-mini', |
| 373 | 376 | deepseek_model: 'deepseek-chat', |
| 374 | 377 | aimlapi_model: 'gpt-4o-mini-2024-07-18', |
| @@ -458,6 +461,7 @@ const oai_settings = { | ||
| 458 | 461 | cohere_model: 'command-r-plus', |
| 459 | 462 | perplexity_model: 'sonar-pro', |
| 460 | 463 | groq_model: 'llama-3.1-70b-versatile', |
| 464 | + electronhub_model: 'gpt-4o-mini', | |
| 461 | 465 | nanogpt_model: 'gpt-4o-mini', |
| 462 | 466 | deepseek_model: 'deepseek-chat', |
| 463 | 467 | aimlapi_model: 'gpt-4-turbo', |
| @@ -1542,6 +1546,11 @@ export function tryParseStreamingError(response, decoded, { quiet = false } = {} | ||
| 1542 | 1546 | !quiet && toastr.error(data.message, 'Chat Completion API'); |
| 1543 | 1547 | throw new Error(data); |
| 1544 | 1548 | } |
| 1549 | + | |
| 1550 | + if (data.detail) { | |
| 1551 | + !quiet && toastr.error(data.detail?.error?.message || response.statusText, 'Chat Completion API'); | |
| 1552 | + throw new Error(data); | |
| 1553 | + } | |
| 1545 | 1554 | } |
| 1546 | 1555 | catch { |
| 1547 | 1556 | // No JSON. Do nothing. |
| @@ -1614,6 +1623,8 @@ export function getChatCompletionModel(source = null) { | ||
| 1614 | 1623 | return oai_settings.perplexity_model; |
| 1615 | 1624 | case chat_completion_sources.GROQ: |
| 1616 | 1625 | return oai_settings.groq_model; |
| 1626 | + case chat_completion_sources.ELECTRONHUB: | |
| 1627 | + return oai_settings.electronhub_model; | |
| 1617 | 1628 | case chat_completion_sources.NANOGPT: |
| 1618 | 1629 | return oai_settings.nanogpt_model; |
| 1619 | 1630 | case chat_completion_sources.DEEPSEEK: |
| @@ -1774,6 +1785,26 @@ function saveModelList(data) { | ||
| 1774 | 1785 | } |
| 1775 | 1786 | } |
| 1776 | 1787 | |
| 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 | + | |
| 1777 | 1808 | if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) { |
| 1778 | 1809 | $('#model_nanogpt_select').empty(); |
| 1779 | 1810 | model_list.forEach((model) => { |
| @@ -2044,12 +2075,14 @@ function getReasoningEffort() { | ||
| 2044 | 2075 | chat_completion_sources.POLLINATIONS, |
| 2045 | 2076 | chat_completion_sources.PERPLEXITY, |
| 2046 | 2077 | chat_completion_sources.COMETAPI, |
| 2078 | + chat_completion_sources.ELECTRONHUB, | |
| 2047 | 2079 | ]; |
| 2048 | 2080 | |
| 2049 | 2081 | if (!reasoningEffortSources.includes(oai_settings.chat_completion_source)) { |
| 2050 | 2082 | return oai_settings.reasoning_effort; |
| 2051 | 2083 | } |
| 2052 | 2084 | |
| 2085 | + function resolveReasoningEffort() { | |
| 2053 | 2086 | switch (oai_settings.reasoning_effort) { |
| 2054 | 2087 | case reasoning_effort_types.auto: |
| 2055 | 2088 | return undefined; |
| @@ -2064,6 +2097,23 @@ function getReasoningEffort() { | ||
| 2064 | 2097 | } |
| 2065 | 2098 | } |
| 2066 | 2099 | |
| 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 | + | |
| 2067 | 2117 | /** |
| 2068 | 2118 | * Send a chat completion request to backend |
| 2069 | 2119 | * @param {string} type (impersonate, quiet, continue, etc) |
| @@ -2100,6 +2150,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = | ||
| 2100 | 2150 | const isGroq = oai_settings.chat_completion_source == chat_completion_sources.GROQ; |
| 2101 | 2151 | const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK; |
| 2102 | 2152 | const isAimlapi = oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI; |
| 2153 | + const isElectronHub = oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB; | |
| 2103 | 2154 | const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI; |
| 2104 | 2155 | const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS; |
| 2105 | 2156 | const isMoonshot = oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT; |
| @@ -2283,6 +2334,11 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = | ||
| 2283 | 2334 | delete generate_data.max_tokens; |
| 2284 | 2335 | } |
| 2285 | 2336 | |
| 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 | + | |
| 2286 | 2342 | const seedSupportedSources = [ |
| 2287 | 2343 | chat_completion_sources.OPENAI, |
| 2288 | 2344 | chat_completion_sources.OPENROUTER, |
| @@ -2290,6 +2346,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = | ||
| 2290 | 2346 | chat_completion_sources.CUSTOM, |
| 2291 | 2347 | chat_completion_sources.COHERE, |
| 2292 | 2348 | chat_completion_sources.GROQ, |
| 2349 | + chat_completion_sources.ELECTRONHUB, | |
| 2293 | 2350 | chat_completion_sources.NANOGPT, |
| 2294 | 2351 | chat_completion_sources.XAI, |
| 2295 | 2352 | chat_completion_sources.POLLINATIONS, |
| @@ -2462,7 +2519,7 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov | ||
| 2462 | 2519 | state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || ''); |
| 2463 | 2520 | } |
| 2464 | 2521 | return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; |
| 2465 | 2522 | } 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)) { |
| 2466 | 2523 | if (show_thoughts) { |
| 2467 | 2524 | state.reasoning += |
| 2468 | 2525 | data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ?? |
| @@ -3424,6 +3481,7 @@ function loadOpenAISettings(data, settings) { | ||
| 3424 | 3481 | oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model; |
| 3425 | 3482 | oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model; |
| 3426 | 3483 | oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model; |
| 3484 | + oai_settings.electronhub_model = settings.electronhub_model ?? default_settings.electronhub_model; | |
| 3427 | 3485 | oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model; |
| 3428 | 3486 | oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model; |
| 3429 | 3487 | oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model; |
| @@ -3519,6 +3577,8 @@ function loadOpenAISettings(data, settings) { | ||
| 3519 | 3577 | $(`#model_perplexity_select option[value="${oai_settings.perplexity_model}"`).prop('selected', true); |
| 3520 | 3578 | $('#model_groq_select').val(oai_settings.groq_model); |
| 3521 | 3579 | $(`#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); | |
| 3522 | 3582 | $('#model_nanogpt_select').val(oai_settings.nanogpt_model); |
| 3523 | 3583 | $(`#model_nanogpt_select option[value="${oai_settings.nanogpt_model}"`).prop('selected', true); |
| 3524 | 3584 | $('#model_deepseek_select').val(oai_settings.deepseek_model); |
| @@ -3805,6 +3865,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { | ||
| 3805 | 3865 | xai_model: settings.xai_model, |
| 3806 | 3866 | pollinations_model: settings.pollinations_model, |
| 3807 | 3867 | aimlapi_model: settings.aimlapi_model, |
| 3868 | + electronhub_model: settings.electronhub_model, | |
| 3808 | 3869 | moonshot_model: settings.moonshot_model, |
| 3809 | 3870 | fireworks_model: settings.fireworks_model, |
| 3810 | 3871 | cometapi_model: settings.cometapi_model, |
| @@ -4571,6 +4632,26 @@ function getFireworksMaxContext(model, isUnlocked) { | ||
| 4571 | 4632 | } |
| 4572 | 4633 | |
| 4573 | 4634 | /** |
| 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 | +/** | |
| 4574 | 4655 | * Get the maximum context size for the NanoGPT model |
| 4575 | 4656 | * @param {string} model Model identifier |
| 4576 | 4657 | * @param {boolean} isUnlocked Whether context limits are unlocked |
| @@ -4681,6 +4762,15 @@ async function onModelChange() { | ||
| 4681 | 4762 | oai_settings.groq_model = value; |
| 4682 | 4763 | } |
| 4683 | 4764 | |
| 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 | + | |
| 4684 | 4774 | if ($(this).is('#model_nanogpt_select')) { |
| 4685 | 4775 | if (!value) { |
| 4686 | 4776 | console.debug('Null NanoGPT model selected. Ignoring.'); |
| @@ -4922,6 +5012,15 @@ async function onModelChange() { | ||
| 4922 | 5012 | $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input'); |
| 4923 | 5013 | } |
| 4924 | 5014 | |
| 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 | + | |
| 4925 | 5024 | if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) { |
| 4926 | 5025 | const maxContext = getNanoGptMaxContext(oai_settings.nanogpt_model, oai_settings.max_context_unlocked); |
| 4927 | 5026 | $('#openai_max_context').attr('max', maxContext); |
| @@ -5216,6 +5315,19 @@ async function onConnectButtonClick(e) { | ||
| 5216 | 5315 | } |
| 5217 | 5316 | } |
| 5218 | 5317 | |
| 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 | + | |
| 5219 | 5331 | if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) { |
| 5220 | 5332 | const api_key_nanogpt = String($('#api_key_nanogpt').val()).trim(); |
| 5221 | 5333 | |
| @@ -5350,6 +5462,9 @@ function toggleChatCompletionForms() { | ||
| 5350 | 5462 | else if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) { |
| 5351 | 5463 | $('#model_groq_select').trigger('change'); |
| 5352 | 5464 | } |
| 5465 | + else if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) { | |
| 5466 | + $('#model_electronhub_select').trigger('change'); | |
| 5467 | + } | |
| 5353 | 5468 | else if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) { |
| 5354 | 5469 | $('#model_nanogpt_select').trigger('change'); |
| 5355 | 5470 | } |
| @@ -5518,6 +5633,8 @@ export function isImageInliningSupported() { | ||
| 5518 | 5633 | return visionSupportedModels.some(model => oai_settings.xai_model.includes(model)); |
| 5519 | 5634 | case chat_completion_sources.AIMLAPI: |
| 5520 | 5635 | 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); | |
| 5521 | 5638 | case chat_completion_sources.POLLINATIONS: |
| 5522 | 5639 | return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.vision); |
| 5523 | 5640 | case chat_completion_sources.COMETAPI: |
| @@ -6326,6 +6443,7 @@ export function initOpenAI() { | ||
| 6326 | 6443 | $('#model_cohere_select').on('change', onModelChange); |
| 6327 | 6444 | $('#model_perplexity_select').on('change', onModelChange); |
| 6328 | 6445 | $('#model_groq_select').on('change', onModelChange); |
| 6446 | + $('#model_electronhub_select').on('change', onModelChange); | |
| 6329 | 6447 | $('#model_nanogpt_select').on('change', onModelChange); |
| 6330 | 6448 | $('#model_deepseek_select').on('change', onModelChange); |
| 6331 | 6449 | $('#model_aimlapi_select').on('change', onModelChange); |
| @@ -51,6 +51,7 @@ export const SECRET_KEYS = { | ||
| 51 | 51 | HUGGINGFACE: 'api_key_huggingface', |
| 52 | 52 | STABILITY: 'api_key_stability', |
| 53 | 53 | CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts', |
| 54 | + ELECTRONHUB: 'api_key_electronhub', | |
| 54 | 55 | NANOGPT: 'api_key_nanogpt', |
| 55 | 56 | TAVILY: 'api_key_tavily', |
| 56 | 57 | BFL: 'api_key_bfl', |
| @@ -95,6 +96,7 @@ const FRIENDLY_NAMES = { | ||
| 95 | 96 | [SECRET_KEYS.GROQ]: 'Groq', |
| 96 | 97 | [SECRET_KEYS.FEATHERLESS]: 'Featherless', |
| 97 | 98 | [SECRET_KEYS.HUGGINGFACE]: 'HuggingFace', |
| 99 | + [SECRET_KEYS.ELECTRONHUB]: 'Electron Hub', | |
| 98 | 100 | [SECRET_KEYS.NANOGPT]: 'NanoGPT', |
| 99 | 101 | [SECRET_KEYS.GENERIC]: 'Generic (OpenAI-compatible)', |
| 100 | 102 | [SECRET_KEYS.DEEPSEEK]: 'DeepSeek', |
| @@ -148,6 +150,7 @@ const INPUT_MAP = { | ||
| 148 | 150 | [SECRET_KEYS.GROQ]: '#api_key_groq', |
| 149 | 151 | [SECRET_KEYS.FEATHERLESS]: '#api_key_featherless', |
| 150 | 152 | [SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface', |
| 153 | + [SECRET_KEYS.ELECTRONHUB]: '#api_key_electronhub', | |
| 151 | 154 | [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt', |
| 152 | 155 | [SECRET_KEYS.GENERIC]: '#api_key_generic', |
| 153 | 156 | [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek', |
| @@ -4841,6 +4841,7 @@ function getModelOptions(quiet) { | ||
| 4841 | 4841 | { id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE }, |
| 4842 | 4842 | { id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY }, |
| 4843 | 4843 | { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ }, |
| 4844 | + { id: 'model_electronhub_select', api: 'openai', type: chat_completion_sources.ELECTRONHUB }, | |
| 4844 | 4845 | { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT }, |
| 4845 | 4846 | { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK }, |
| 4846 | 4847 | { id: 'model_aimlapi_select', api: 'openai', type: chat_completion_sources.AIMLAPI }, |
| @@ -633,6 +633,13 @@ export class ToolManager { | ||
| 633 | 633 | } |
| 634 | 634 | } |
| 635 | 635 | |
| 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 | + | |
| 636 | 643 | const supportedSources = [ |
| 637 | 644 | chat_completion_sources.OPENAI, |
| 638 | 645 | chat_completion_sources.CUSTOM, |
| @@ -651,6 +658,7 @@ export class ToolManager { | ||
| 651 | 658 | chat_completion_sources.MOONSHOT, |
| 652 | 659 | chat_completion_sources.FIREWORKS, |
| 653 | 660 | chat_completion_sources.COMETAPI, |
| 661 | + chat_completion_sources.ELECTRONHUB, | |
| 654 | 662 | ]; |
| 655 | 663 | return supportedSources.includes(oai_settings.chat_completion_source); |
| 656 | 664 | } |
| @@ -173,6 +173,7 @@ export const CHAT_COMPLETION_SOURCES = { | ||
| 173 | 173 | COHERE: 'cohere', |
| 174 | 174 | PERPLEXITY: 'perplexity', |
| 175 | 175 | GROQ: 'groq', |
| 176 | + ELECTRONHUB: 'electronhub', | |
| 176 | 177 | NANOGPT: 'nanogpt', |
| 177 | 178 | DEEPSEEK: 'deepseek', |
| 178 | 179 | AIMLAPI: 'aimlapi', |
| @@ -61,6 +61,7 @@ const API_GROQ = 'https://api.groq.com/openai/v1'; | ||
| 61 | 61 | const API_MAKERSUITE = 'https://generativelanguage.googleapis.com'; |
| 62 | 62 | const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com'; |
| 63 | 63 | const API_AI21 = 'https://api.ai21.com/studio/v1'; |
| 64 | +const API_ELECTRONHUB = 'https://api.electronhub.ai/v1'; | |
| 64 | 65 | const API_NANOGPT = 'https://nano-gpt.com/api/v1'; |
| 65 | 66 | const API_DEEPSEEK = 'https://api.deepseek.com/beta'; |
| 66 | 67 | const API_XAI = 'https://api.x.ai/v1'; |
| @@ -1193,6 +1194,106 @@ async function sendAimlapiRequest(request, response) { | ||
| 1193 | 1194 | } |
| 1194 | 1195 | } |
| 1195 | 1196 | |
| 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 | + | |
| 1196 | 1297 | export const router = express.Router(); |
| 1197 | 1298 | |
| 1198 | 1299 | router.post('/status', async function (request, statusResponse) { |
| @@ -1225,6 +1326,10 @@ router.post('/status', async function (request, statusResponse) { | ||
| 1225 | 1326 | apiUrl = API_COHERE_V1; |
| 1226 | 1327 | apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE); |
| 1227 | 1328 | 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 = {}; | |
| 1228 | 1333 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) { |
| 1229 | 1334 | apiUrl = API_NANOGPT; |
| 1230 | 1335 | apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT); |
| @@ -1490,6 +1595,7 @@ router.post('/generate', function (request, response) { | ||
| 1490 | 1595 | case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response); |
| 1491 | 1596 | case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response); |
| 1492 | 1597 | case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response); |
| 1598 | + case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return sendElectronHubRequest(request, response); | |
| 1493 | 1599 | } |
| 1494 | 1600 | |
| 1495 | 1601 | let apiUrl; |
| @@ -1927,4 +2033,22 @@ multimodalModels.post('/nanogpt', async (_req, res) => { | ||
| 1927 | 2033 | } |
| 1928 | 2034 | }); |
| 1929 | 2035 | |
| 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 | + | |
| 1930 | 2054 | router.use('/multimodal-models', multimodalModels); |
| @@ -81,6 +81,10 @@ router.post('/caption-image', async (request, response) => { | ||
| 81 | 81 | key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT); |
| 82 | 82 | } |
| 83 | 83 | |
| 84 | + if (request.body.api === 'electronhub') { | |
| 85 | + key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB); | |
| 86 | + } | |
| 87 | + | |
| 84 | 88 | const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp', 'pollinations']; |
| 85 | 89 | if (!key && !request.body.reverse_proxy && !noKeyTypes.includes(request.body.api)) { |
| 86 | 90 | console.warn('No key found for API', request.body.api); |
| @@ -169,6 +173,10 @@ router.post('/caption-image', async (request, response) => { | ||
| 169 | 173 | apiUrl = 'https://nano-gpt.com/api/v1/chat/completions'; |
| 170 | 174 | } |
| 171 | 175 | |
| 176 | + if (request.body.api === 'electronhub') { | |
| 177 | + apiUrl = 'https://api.electronhub.ai/v1/chat/completions'; | |
| 178 | + } | |
| 179 | + | |
| 172 | 180 | if (['koboldcpp', 'vllm', 'llamacpp', 'ooba'].includes(request.body.api)) { |
| 173 | 181 | apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`; |
| 174 | 182 | } |
| @@ -45,6 +45,7 @@ export const SECRET_KEYS = { | ||
| 45 | 45 | STABILITY: 'api_key_stability', |
| 46 | 46 | CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts', |
| 47 | 47 | TAVILY: 'api_key_tavily', |
| 48 | + ELECTRONHUB: 'api_key_electronhub', | |
| 48 | 49 | NANOGPT: 'api_key_nanogpt', |
| 49 | 50 | BFL: 'api_key_bfl', |
| 50 | 51 | FALAI: 'api_key_falai', |
| @@ -957,6 +957,118 @@ huggingface.post('/generate', async (request, response) => { | ||
| 957 | 957 | } |
| 958 | 958 | }); |
| 959 | 959 | |
| 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 | + | |
| 960 | 1072 | const nanogpt = express.Router(); |
| 961 | 1073 | |
| 962 | 1074 | nanogpt.post('/models', async (request, response) => { |
| @@ -1439,6 +1551,7 @@ router.use('/drawthings', drawthings); | ||
| 1439 | 1551 | router.use('/pollinations', pollinations); |
| 1440 | 1552 | router.use('/stability', stability); |
| 1441 | 1553 | router.use('/huggingface', huggingface); |
| 1554 | +router.use('/electronhub', electronhub); | |
| 1442 | 1555 | router.use('/nanogpt', nanogpt); |
| 1443 | 1556 | router.use('/bfl', bfl); |
| 1444 | 1557 | router.use('/falai', falai); |