feat: [Electron Hub] Added Text-to-Speech, Prompt cost, Sort/Group/Se… (#4528) * feat: [Electron Hub] Added Text-to-Speech, Prompt cost, Sort/Group/Search for model list * feat: [Electron Hub] Added Text-to-Speech, Prompt cost, Sort/Group/Search for model list * Update public/scripts/extensions/tts/electronhub.js Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update public/scripts/openai.js Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update public/scripts/openai.js Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * feat: [Electron Hub] Added Text-to-Speech, Prompt cost, Sort/Group/Search for model list * feat: [Electron Hub] Show model capabilities * Support logit_bias * Small tweaks * Added tokenizer selection logic * Added tokenizer selection logic * Fixed ESLint * Small tweaks * Split localization tags * Fix formatting * Refactor icons, add tool icon * Support newer oai model tokenizers --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -8,6 +8,9 @@ | ||
| 8 | 8 | "openrouter_sort_models": "alphabetically", |
| 9 | 9 | "ai21_model": "jamba-large", |
| 10 | 10 | "mistralai_model": "mistral-large-latest", |
| 11 | + "electronhub_model": "gpt-4o-mini", | |
| 12 | + "electronhub_sort_models": "alphabetically", | |
| 13 | + "electronhub_group_models": false, | |
| 11 | 14 | "custom_model": "", |
| 12 | 15 | "custom_url": "", |
| 13 | 16 | "custom_include_body": "", |
| @@ -676,6 +676,9 @@ | ||
| 676 | 676 | <div data-source="openrouter"> |
| 677 | 677 | <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="openrouter_max_prompt_cost" data-i18n="Unknown">Unknown</span> |
| 678 | 678 | </div> |
| 679 | + <div data-source="electronhub"> | |
| 680 | + <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="electronhub_max_prompt_cost" data-i18n="Unknown">Unknown</span> | |
| 681 | + </div> | |
| 679 | 682 | <hr> |
| 680 | 683 | <div class="range-block"> |
| 681 | 684 | <label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand"> |
| @@ -2123,7 +2126,7 @@ | ||
| 2123 | 2126 | </div> |
| 2124 | 2127 | </div> |
| 2125 | 2128 | </div> |
| 2126 | 2129 | <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai"> |
| 2127 | 2130 | <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias"> |
| 2128 | 2131 | Logit Bias |
| 2129 | 2132 | </div> |
| @@ -3061,7 +3064,7 @@ | ||
| 3061 | 3064 | <div class="marginTopBot5"> |
| 3062 | 3065 | <div class="inline-drawer wide100p"> |
| 3063 | 3066 | <div class="inline-drawer-toggle inline-drawer-header"> |
| 3064 | 3067 | <b data-i18n="OpenRouter Model OrderSorting">OpenRouter Model Sorting</b> |
| 3065 | 3068 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> |
| 3066 | 3069 | </div> |
| 3067 | 3070 | <div class="inline-drawer-content m-b-1"> |
| @@ -3452,6 +3455,9 @@ | ||
| 3452 | 3455 | </div> |
| 3453 | 3456 | <div id="electronhub_form" data-source="electronhub"> |
| 3454 | 3457 | <h4 data-i18n="Electron Hub API Key">Electron Hub API Key</h4> |
| 3458 | + <div> | |
| 3459 | + <a href="https://playground.electronhub.ai/console" target="_blank" data-i18n="View Remaining Credits">View Remaining Credits</a> | |
| 3460 | + </div> | |
| 3455 | 3461 | <div class="flex-container"> |
| 3456 | 3462 | <input id="api_key_electronhub" name="api_key_electronhub" class="text_pole flex1" value="" type="text" autocomplete="off"> |
| 3457 | 3463 | <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> |
| @@ -3459,10 +3465,43 @@ | ||
| 3459 | 3465 | <div data-for="api_key_electronhub" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'."> |
| 3460 | 3466 | For privacy reasons, your API key will be hidden after you click 'Connect'. |
| 3461 | 3467 | </div> |
| 3462 | - <h4 data-i18n="Electron Hub Model">Electron Hub Model</h4> | |
| 3468 | + <div> | |
| 3463 | - <select id="model_electronhub_select"> | |
| 3469 | + <h4 data-i18n="Electron Hub Model">Electron Hub Model</h4> | |
| 3464 | - <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> | |
| 3470 | + <select id="model_electronhub_select"> | |
| 3465 | - </select> | |
| 3471 | + <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> | |
| 3472 | + </select> | |
| 3473 | + </div> | |
| 3474 | + <div class="marginTopBot5"> | |
| 3475 | + <div class="inline-drawer wide100p"> | |
| 3476 | + <div class="inline-drawer-toggle inline-drawer-header"> | |
| 3477 | + <b data-i18n="Electron Hub Model Sorting">Electron Hub Model Sorting</b> | |
| 3478 | + <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> | |
| 3479 | + </div> | |
| 3480 | + <div class="inline-drawer-content m-b-1"> | |
| 3481 | + <div class="marginTopBot5"> | |
| 3482 | + <label for="electronhub_sort_models" class="checkbox_label"> | |
| 3483 | + <select id="electronhub_sort_models"> | |
| 3484 | + <option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option> | |
| 3485 | + <option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option> | |
| 3486 | + <option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option> | |
| 3487 | + <option data-i18n="Context Size" value="context_length">Context Size</option> | |
| 3488 | + </select> | |
| 3489 | + </label> | |
| 3490 | + </div> | |
| 3491 | + <div class="marginTopBot5"> | |
| 3492 | + <label for="electronhub_group_models" class="checkbox_label"> | |
| 3493 | + <input id="electronhub_group_models" type="checkbox" /> | |
| 3494 | + <span data-i18n="Group by vendors">Group by vendors</span> | |
| 3495 | + </label> | |
| 3496 | + <div class="toggle-description justifyLeft wide100p"> | |
| 3497 | + <span data-i18n="Group by vendors Description"> | |
| 3498 | + Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting. | |
| 3499 | + </span> | |
| 3500 | + </div> | |
| 3501 | + </div> | |
| 3502 | + </div> | |
| 3503 | + </div> | |
| 3504 | + </div> | |
| 3466 | 3505 | </div> |
| 3467 | 3506 | <div id="nanogpt_form" data-source="nanogpt"> |
| 3468 | 3507 | <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4> |
| @@ -7759,4 +7798,4 @@ | ||
| 7759 | 7798 | </script> |
| 7760 | 7799 | </body> |
| 7761 | 7800 | |
| 7762 | -</html> | |
| 7762 | \ No newline at end of file | |
| 7801 | +</html> | |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "وحدة تحكم المطور في Anthropic", |
| 370 | 370 | "Claude Model": "نموذج Claude", |
| 371 | 371 | "Window AI Model": "نموذج Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "فرز نموذج OpenRouter", |
| 373 | 373 | "Alphabetically": "أبجديا", |
| 374 | 374 | "Price": "السعر (الأرخص)", |
| 375 | 375 | "Context Size": "حجم السياق", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "Anthropics Entwicklerkonsole", |
| 370 | 370 | "Claude Model": "Claude-Modell", |
| 371 | 371 | "Window AI Model": "Fenster AI-Modell", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Sortierung des OpenRouter-Modells", |
| 373 | 373 | "Alphabetically": "Alphabetisch", |
| 374 | 374 | "Price": "Preis (am günstigsten)", |
| 375 | 375 | "Context Size": "Kontextgröße", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "la consola de desarrolladores de Anthropic", |
| 370 | 370 | "Claude Model": "Modelo de Claude", |
| 371 | 371 | "Window AI Model": "Modelo de Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Clasificación de modelos de OpenRouter", |
| 373 | 373 | "Alphabetically": "Alfabéticamente", |
| 374 | 374 | "Price": "Precio (más barato)", |
| 375 | 375 | "Context Size": "Tamaño del contexto", |
| @@ -348,7 +348,7 @@ | ||
| 348 | 348 | "Anthropic's developer console": "Console de développement d'Anthropic", |
| 349 | 349 | "Claude Model": "Modèle Claude", |
| 350 | 350 | "Window AI Model": "Modèle Window AI", |
| 351 | 351 | "OpenRouter Model OrderSorting": "Tri des modèles OpenRouter", |
| 352 | 352 | "Alphabetically": "Alphabétiquement", |
| 353 | 353 | "Price": "Prix (le moins cher)", |
| 354 | 354 | "Context Size": "Taille du contexte", |
| @@ -367,7 +367,7 @@ | ||
| 367 | 367 | "Anthropic's developer console": "Uppbyggingaraðilar Forritara stjórnborð", |
| 368 | 368 | "Claude Model": "Claude módel", |
| 369 | 369 | "Window AI Model": "Vindauga AI módel", |
| 370 | 370 | "OpenRouter Model OrderSorting": "OpenRouter líkanaflokkun", |
| 371 | 371 | "Alphabetically": "Stafrófsröð", |
| 372 | 372 | "Price": "Verð (ódýrast)", |
| 373 | 373 | "Context Size": "Samhengisstærð", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "Console dello sviluppatore di Anthropic", |
| 370 | 370 | "Claude Model": "Modello Claude", |
| 371 | 371 | "Window AI Model": "Modello AI di Window", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Ordinamento dei modelli OpenRouter", |
| 373 | 373 | "Alphabetically": "In ordine alfabetico", |
| 374 | 374 | "Price": "Prezzo (il più economico)", |
| 375 | 375 | "Context Size": "Dimensione del contesto", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "Anthropicの開発者コンソール", |
| 370 | 370 | "Claude Model": "Claudeモデル", |
| 371 | 371 | "Window AI Model": "Window AIモデル", |
| 372 | 372 | "OpenRouter Model OrderSorting": "OpenRouter モデルのソート", |
| 373 | 373 | "Alphabetically": "アルファベット順", |
| 374 | 374 | "Price": "価格(最安値)", |
| 375 | 375 | "Context Size": "コンテキストサイズ", |
| @@ -374,7 +374,7 @@ | ||
| 374 | 374 | "Slack and Poe cookies will not work here, do not bother trying.": "Slack과 Poe 쿠키는 여기서 작동하지 않습니다. 시도하지 마세요.", |
| 375 | 375 | "Claude Model": "Claude 모델", |
| 376 | 376 | "Window AI Model": "Window AI 모델", |
| 377 | 377 | "OpenRouter Model OrderSorting": "OpenRouter 모델 정렬", |
| 378 | 378 | "Alphabetically": "알파벳순", |
| 379 | 379 | "Price": "가격(가장 저렴)", |
| 380 | 380 | "Context Size": "컨텍스트 크기", |
| @@ -367,7 +367,7 @@ | ||
| 367 | 367 | "Anthropic's developer console": "Anthropic's ontwikkelaarsconsole", |
| 368 | 368 | "Claude Model": "Claude-model", |
| 369 | 369 | "Window AI Model": "Window AI-model", |
| 370 | 370 | "OpenRouter Model OrderSorting": "Sorteren van OpenRouter-modellen", |
| 371 | 371 | "Alphabetically": "Alfabetisch", |
| 372 | 372 | "Price": "Prijs (goedkoopste)", |
| 373 | 373 | "Context Size": "Contextgrootte", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "console de desenvolvedor da Anthropic", |
| 370 | 370 | "Claude Model": "Modelo Claude", |
| 371 | 371 | "Window AI Model": "Modelo Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Classificação de modelo OpenRouter", |
| 373 | 373 | "Alphabetically": "Alfabeticamente", |
| 374 | 374 | "Price": "Preço (mais barato)", |
| 375 | 375 | "Context Size": "Tamanho do contexto", |
| @@ -718,7 +718,7 @@ | ||
| 718 | 718 | "Google Model": "Модель Google", |
| 719 | 719 | "Cohere API Key": "Ключ от API Cohere", |
| 720 | 720 | "Cohere Model": "Модель Cohere", |
| 721 | 721 | "OpenRouter Model OrderSorting": "Сортировка моделей OpenRouter", |
| 722 | 722 | "Alphabetically": "По алфавиту", |
| 723 | 723 | "Price": "По цене (наиболее низкая)", |
| 724 | 724 | "Context Size": "По размеру контекста", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "консолі розробника Anthropic", |
| 370 | 370 | "Claude Model": "Модель Claude", |
| 371 | 371 | "Window AI Model": "Модель Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Сортування моделі OpenRouter", |
| 373 | 373 | "Alphabetically": "За алфавітом", |
| 374 | 374 | "Price": "Ціна (найдешевша)", |
| 375 | 375 | "Context Size": "Розмір контексту", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "developer console của Anthropic", |
| 370 | 370 | "Claude Model": "Model Claude", |
| 371 | 371 | "Window AI Model": "Model Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Sắp xếp model OpenRouter", |
| 373 | 373 | "Alphabetically": "Theo thứ tự bảng chữ cái", |
| 374 | 374 | "Price": "Giá (rẻ nhất)", |
| 375 | 375 | "Context Size": "Kích thước bối cảnh", |
| @@ -452,7 +452,7 @@ | ||
| 452 | 452 | "Claude Model": "Claude 模型", |
| 453 | 453 | "Allow fallback routes Description": "如果所选模型无法响应您的请求,则自动选择备用模型。", |
| 454 | 454 | "Allow fallback models": "允许后备模型", |
| 455 | 455 | "OpenRouter Model OrderSorting": "OpenRouter 模型顺序", |
| 456 | 456 | "Alphabetically": "按字母顺序", |
| 457 | 457 | "Price": "价格(最便宜)", |
| 458 | 458 | "Context Size": "上下文长度", |
| @@ -368,7 +368,7 @@ | ||
| 368 | 368 | "Anthropic's developer console": "Anthropic 的開發者控制台", |
| 369 | 369 | "Claude Model": "Claude 模型", |
| 370 | 370 | "Window AI Model": "Window AI 模型", |
| 371 | 371 | "OpenRouter Model OrderSorting": "模型順序", |
| 372 | 372 | "Alphabetically": "按字母順序", |
| 373 | 373 | "Price": "價格(最便宜的)", |
| 374 | 374 | "Context Size": "上下文長度", |
| @@ -0,0 +1,455 @@ | ||
| 1 | +import { event_types, eventSource, getRequestHeaders } from '../../../script.js'; | |
| 2 | +import { SECRET_KEYS, secret_state } from '../../secrets.js'; | |
| 3 | +import { getPreviewString, saveTtsProviderSettings, initVoiceMap } from './index.js'; | |
| 4 | + | |
| 5 | +export { ElectronHubTtsProvider }; | |
| 6 | + | |
| 7 | +class ElectronHubTtsProvider { | |
| 8 | + settings; | |
| 9 | + voices = []; | |
| 10 | + models = []; | |
| 11 | + separator = ' . '; | |
| 12 | + audioElement = document.createElement('audio'); | |
| 13 | + | |
| 14 | + defaultSettings = { | |
| 15 | + voiceMap: {}, | |
| 16 | + model: 'tts-1', | |
| 17 | + speed: 1, | |
| 18 | + temperature: 1, | |
| 19 | + top_p: 1, | |
| 20 | + // GPT-4o Mini TTS | |
| 21 | + instructions: '', | |
| 22 | + // Dia | |
| 23 | + speaker_transcript: '', | |
| 24 | + cfg_filter_top_k: 25, | |
| 25 | + cfg_scale: 3, | |
| 26 | + // Microsoft TTS | |
| 27 | + speech_rate: 0, | |
| 28 | + pitch_adjustment: 0, | |
| 29 | + emotional_style: '', | |
| 30 | + }; | |
| 31 | + | |
| 32 | + get settingsHtml() { | |
| 33 | + let html = ` | |
| 34 | + <div>Electron Hub unified TTS API.</div> | |
| 35 | + <div class="flex-container alignItemsCenter"> | |
| 36 | + <div class="flex1"></div> | |
| 37 | + <div id="electronhub_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_electronhub"> | |
| 38 | + <i class="fa-solid fa-key"></i> | |
| 39 | + <span>API Key</span> | |
| 40 | + </div> | |
| 41 | + </div> | |
| 42 | + <div class="flex-container flexGap10 wrap"> | |
| 43 | + <div class="flex1"> | |
| 44 | + <label for="electronhub_tts_model">Model</label> | |
| 45 | + <select id="electronhub_tts_model" class="text_pole"></select> | |
| 46 | + </div> | |
| 47 | + <div> | |
| 48 | + <label for="electronhub_tts_speed">Speed <span id="electronhub_tts_speed_output"></span></label> | |
| 49 | + <input type="range" id="electronhub_tts_speed" value="1" min="0.25" max="4" step="0.05"> | |
| 50 | + </div> | |
| 51 | + <div> | |
| 52 | + <label for="electronhub_tts_temperature">Temperature</label> | |
| 53 | + <input id="electronhub_tts_temperature" class="text_pole" type="number" min="0" max="2" step="0.1" value="1" /> | |
| 54 | + </div> | |
| 55 | + <div id="electronhub_block_top_p" style="display:none;"> | |
| 56 | + <label for="electronhub_tts_top_p">Top-p</label> | |
| 57 | + <input id="electronhub_tts_top_p" class="text_pole" type="number" min="0" max="1" step="0.01" value="1" /> | |
| 58 | + </div> | |
| 59 | + </div> | |
| 60 | + | |
| 61 | + <div id="electronhub_block_instructions" style="display:none;"> | |
| 62 | + <label for="electronhub_tts_instructions">Instructions (GPT-4o Mini TTS):</label> | |
| 63 | + <textarea id="electronhub_tts_instructions" class="textarea_compact autoSetHeight" placeholder="e.g., 'Speak cheerfully and energetically'"></textarea> | |
| 64 | + </div> | |
| 65 | + | |
| 66 | + <div id="electronhub_block_dia" style="display:none;"> | |
| 67 | + <label for="electronhub_tts_speaker_transcript">Speaker transcript (Dia):</label> | |
| 68 | + <textarea id="electronhub_tts_speaker_transcript" class="textarea_compact autoSetHeight" maxlength="1000"></textarea> | |
| 69 | + <label for="electronhub_tts_cfg_scale">CFG scale (1-5):</label> | |
| 70 | + <input id="electronhub_tts_cfg_scale" type="number" min="1" max="5" step="1" /> | |
| 71 | + <label for="electronhub_tts_cfg_topk">CFG filter top_k (15-50):</label> | |
| 72 | + <input id="electronhub_tts_cfg_topk" type="number" min="15" max="50" step="1" /> | |
| 73 | + </div> | |
| 74 | + | |
| 75 | + <div id="electronhub_block_msft" style="display:none;"> | |
| 76 | + <div class="flex-container flexGap10 wrap"> | |
| 77 | + <div> | |
| 78 | + <label for="electronhub_tts_speech_rate">Speech rate (-100..100)</label> | |
| 79 | + <input id="electronhub_tts_speech_rate" class="text_pole" type="number" min="-100" max="100" step="1" style="width:120px;" /> | |
| 80 | + </div> | |
| 81 | + <div> | |
| 82 | + <label for="electronhub_tts_pitch_adjustment">Pitch adjustment (-100..100)</label> | |
| 83 | + <input id="electronhub_tts_pitch_adjustment" class="text_pole" type="number" min="-100" max="100" step="1" style="width:120px;" /> | |
| 84 | + </div> | |
| 85 | + </div> | |
| 86 | + <div class="flex-container flexGap10"> | |
| 87 | + <div class="flex1"> | |
| 88 | + <label for="electronhub_tts_emotional_style">Emotional style</label> | |
| 89 | + <input id="electronhub_tts_emotional_style" class="text_pole" type="text" placeholder="cheerful, sad, angry, gentle..." /> | |
| 90 | + </div> | |
| 91 | + </div> | |
| 92 | + </div> | |
| 93 | + | |
| 94 | + <div id="electronhub_dynamic_params" class="flex-container flexGap10 wrap" style="display:none;"></div>`; | |
| 95 | + return html; | |
| 96 | + } | |
| 97 | + | |
| 98 | + constructor() { | |
| 99 | + this.handler = async function (/** @type {string} */ key) { | |
| 100 | + if (key !== SECRET_KEYS.ELECTRONHUB) return; | |
| 101 | + $('#electronhub_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELECTRONHUB]); | |
| 102 | + await this.onRefreshClick(); | |
| 103 | + }.bind(this); | |
| 104 | + } | |
| 105 | + | |
| 106 | + dispose() { | |
| 107 | + [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { | |
| 108 | + eventSource.removeListener(event, this.handler); | |
| 109 | + }); | |
| 110 | + } | |
| 111 | + | |
| 112 | + async loadSettings(settings) { | |
| 113 | + if (Object.keys(settings).length == 0) { | |
| 114 | + console.info('Using default Electron Hub TTS settings'); | |
| 115 | + } | |
| 116 | + | |
| 117 | + this.settings = { ...this.defaultSettings, ...settings }; | |
| 118 | + | |
| 119 | + await this.loadModels(); | |
| 120 | + this.populateModelSelect(); | |
| 121 | + | |
| 122 | + $('#electronhub_tts_model').val(this.settings.model); | |
| 123 | + $('#electronhub_tts_model').on('change', () => { this.onSettingsChange(); }); | |
| 124 | + | |
| 125 | + $('#electronhub_tts_speed').val(this.settings.speed); | |
| 126 | + $('#electronhub_tts_speed_output').text(this.settings.speed); | |
| 127 | + $('#electronhub_tts_speed').on('input', () => { this.onSettingsChange(); }); | |
| 128 | + | |
| 129 | + $('#electronhub_tts_temperature').val(this.settings.temperature); | |
| 130 | + $('#electronhub_tts_temperature').on('input', () => { this.onSettingsChange(); }); | |
| 131 | + | |
| 132 | + $('#electronhub_tts_top_p').val(this.settings.top_p); | |
| 133 | + $('#electronhub_tts_top_p').on('input', () => { this.onSettingsChange(); }); | |
| 134 | + | |
| 135 | + $('#electronhub_tts_instructions').val(this.settings.instructions); | |
| 136 | + $('#electronhub_tts_instructions').on('input', () => { this.onSettingsChange(); }); | |
| 137 | + | |
| 138 | + $('#electronhub_tts_speaker_transcript').val(this.settings.speaker_transcript); | |
| 139 | + $('#electronhub_tts_speaker_transcript').on('input', () => { this.onSettingsChange(); }); | |
| 140 | + $('#electronhub_tts_cfg_scale').val(this.settings.cfg_scale); | |
| 141 | + $('#electronhub_tts_cfg_scale').on('input', () => { this.onSettingsChange(); }); | |
| 142 | + $('#electronhub_tts_cfg_topk').val(this.settings.cfg_filter_top_k); | |
| 143 | + $('#electronhub_tts_cfg_topk').on('input', () => { this.onSettingsChange(); }); | |
| 144 | + | |
| 145 | + $('#electronhub_tts_speech_rate').val(this.settings.speech_rate); | |
| 146 | + $('#electronhub_tts_speech_rate').on('input', () => { this.onSettingsChange(); }); | |
| 147 | + $('#electronhub_tts_pitch_adjustment').val(this.settings.pitch_adjustment); | |
| 148 | + $('#electronhub_tts_pitch_adjustment').on('input', () => { this.onSettingsChange(); }); | |
| 149 | + $('#electronhub_tts_emotional_style').val(this.settings.emotional_style); | |
| 150 | + $('#electronhub_tts_emotional_style').on('input', () => { this.onSettingsChange(); }); | |
| 151 | + | |
| 152 | + $('#electronhub_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELECTRONHUB]); | |
| 153 | + [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { | |
| 154 | + eventSource.on(event, this.handler); | |
| 155 | + }); | |
| 156 | + | |
| 157 | + await this.checkReady(); | |
| 158 | + this.updateConditionalBlocks(); | |
| 159 | + this.renderDynamicParams(); | |
| 160 | + console.debug('Electron Hub TTS: Settings loaded'); | |
| 161 | + } | |
| 162 | + | |
| 163 | + async onSettingsChange() { | |
| 164 | + const previousModel = this.settings.model; | |
| 165 | + this.settings.model = String($('#electronhub_tts_model').find(':selected').val() || this.settings.model); | |
| 166 | + this.settings.speed = Number($('#electronhub_tts_speed').val()); | |
| 167 | + $('#electronhub_tts_speed_output').text(this.settings.speed); | |
| 168 | + this.settings.temperature = Number($('#electronhub_tts_temperature').val()); | |
| 169 | + this.settings.top_p = Number($('#electronhub_tts_top_p').val()); | |
| 170 | + this.settings.instructions = String($('#electronhub_tts_instructions').val() || ''); | |
| 171 | + this.settings.speaker_transcript = String($('#electronhub_tts_speaker_transcript').val() || ''); | |
| 172 | + this.settings.cfg_scale = Number($('#electronhub_tts_cfg_scale').val()); | |
| 173 | + this.settings.cfg_filter_top_k = Number($('#electronhub_tts_cfg_topk').val()); | |
| 174 | + this.settings.speech_rate = Number($('#electronhub_tts_speech_rate').val()); | |
| 175 | + this.settings.pitch_adjustment = Number($('#electronhub_tts_pitch_adjustment').val()); | |
| 176 | + this.settings.emotional_style = String($('#electronhub_tts_emotional_style').val() || ''); | |
| 177 | + this.updateConditionalBlocks(); | |
| 178 | + this.renderDynamicParams(); | |
| 179 | + saveTtsProviderSettings(); | |
| 180 | + if (previousModel !== this.settings.model) { | |
| 181 | + this.voices = await this.fetchTtsVoiceObjects(); | |
| 182 | + await initVoiceMap(); | |
| 183 | + } | |
| 184 | + } | |
| 185 | + | |
| 186 | + async loadModels() { | |
| 187 | + try { | |
| 188 | + const response = await fetch('/api/openai/electronhub/models', { | |
| 189 | + method: 'POST', | |
| 190 | + headers: getRequestHeaders(), | |
| 191 | + }); | |
| 192 | + if (!response.ok) { | |
| 193 | + throw new Error(`HTTP ${response.status}: ${await response.text()}`); | |
| 194 | + } | |
| 195 | + /** @type {Array<any>} */ | |
| 196 | + const data = await response.json(); | |
| 197 | + const allModels = Array.isArray(data) ? data : []; | |
| 198 | + const ttsModels = allModels.filter(m => { | |
| 199 | + const eps = Array.isArray(m?.endpoints) ? m.endpoints : []; | |
| 200 | + return eps.some(ep => { | |
| 201 | + if (typeof ep !== 'string') return false; | |
| 202 | + return ep === '/v1/audio/speech' || ep.endsWith('/audio/speech') || ep === 'audio/speech'; | |
| 203 | + }); | |
| 204 | + }); | |
| 205 | + | |
| 206 | + this.models = ttsModels; | |
| 207 | + | |
| 208 | + if (this.models.length > 0 && !this.models.find(m => m.id === this.settings.model)) { | |
| 209 | + this.settings.model = this.models[0].id; | |
| 210 | + saveTtsProviderSettings(); | |
| 211 | + } | |
| 212 | + } catch (err) { | |
| 213 | + console.warn('Electron Hub models fetch failed', err); | |
| 214 | + this.models = []; | |
| 215 | + } | |
| 216 | + } | |
| 217 | + | |
| 218 | + populateModelSelect() { | |
| 219 | + const select = $('#electronhub_tts_model'); | |
| 220 | + select.empty(); | |
| 221 | + const groups = this.groupByVendor(this.models); | |
| 222 | + for (const [vendor, models] of groups.entries()) { | |
| 223 | + const optgroup = document.createElement('optgroup'); | |
| 224 | + optgroup.label = vendor; | |
| 225 | + for (const m of models) { | |
| 226 | + const opt = document.createElement('option'); | |
| 227 | + opt.value = m.id; | |
| 228 | + opt.text = m.name || m.id; | |
| 229 | + optgroup.appendChild(opt); | |
| 230 | + } | |
| 231 | + select.append(optgroup); | |
| 232 | + } | |
| 233 | + | |
| 234 | + if (this.models.find(x => x.id === this.settings.model)) { | |
| 235 | + select.val(this.settings.model); | |
| 236 | + } | |
| 237 | + } | |
| 238 | + | |
| 239 | + /** | |
| 240 | + * Group models by vendor prefix from name before ':' | |
| 241 | + * @param {Array<any>} array | |
| 242 | + * @returns {Map<string, any[]>} | |
| 243 | + */ | |
| 244 | + groupByVendor(array) { | |
| 245 | + return array.reduce((acc, curr) => { | |
| 246 | + const name = String(curr?.name || curr?.id || 'Other'); | |
| 247 | + const vendor = name.split(':')[0].trim() || 'Other'; | |
| 248 | + if (!acc.has(vendor)) acc.set(vendor, []); | |
| 249 | + acc.get(vendor).push(curr); | |
| 250 | + return acc; | |
| 251 | + }, new Map()); | |
| 252 | + } | |
| 253 | + | |
| 254 | + updateConditionalBlocks() { | |
| 255 | + const modelId = this.settings.model; | |
| 256 | + const model = this.models.find(m => m.id === modelId); | |
| 257 | + const params = model?.parameters || {}; | |
| 258 | + const vendorName = String(model?.name || '').split(':')[0].trim().toLowerCase(); | |
| 259 | + | |
| 260 | + const hasInstructions = 'instructions' in params || modelId === 'gpt-4o-mini-tts'; | |
| 261 | + const hasDia = 'speaker_transcript' in params || 'cfg_scale' in params || 'cfg_filter_top_k' in params || modelId.includes('dia'); | |
| 262 | + | |
| 263 | + const hasMsft = 'speech_rate' in params || 'pitch_adjustment' in params || 'emotional_style' in params || vendorName === 'microsoft' || modelId === 'microsoft-tts'; | |
| 264 | + const hasTopP = 'top_p' in params; | |
| 265 | + | |
| 266 | + $('#electronhub_block_instructions').toggle(!!hasInstructions); | |
| 267 | + $('#electronhub_block_dia').toggle(!!hasDia); | |
| 268 | + $('#electronhub_block_msft').toggle(!!hasMsft); | |
| 269 | + $('#electronhub_block_top_p').toggle(!!hasTopP); | |
| 270 | + } | |
| 271 | + | |
| 272 | + /** | |
| 273 | + * Build UI for additional model parameters dynamically | |
| 274 | + */ | |
| 275 | + renderDynamicParams() { | |
| 276 | + const container = $('#electronhub_dynamic_params'); | |
| 277 | + container.empty(); | |
| 278 | + const model = this.models.find(m => m.id === this.settings.model); | |
| 279 | + const params = model?.parameters || {}; | |
| 280 | + const modelHasVoices = Array.isArray(model?.voices) && model.voices.length > 0; | |
| 281 | + const exclude = new Set(['input', 'response_format', 'model', 'speed', 'temperature', 'top_p', 'instructions', 'speaker_transcript', 'cfg_scale', 'cfg_filter_top_k', 'speech_rate', 'pitch_adjustment', 'emotional_style']); | |
| 282 | + if (modelHasVoices) exclude.add('voice'); | |
| 283 | + | |
| 284 | + const entries = Object.entries(params).filter(([k]) => !exclude.has(k)); | |
| 285 | + container.toggle(entries.length > 0); | |
| 286 | + if (entries.length === 0) return; | |
| 287 | + | |
| 288 | + for (const [key, spec] of entries) { | |
| 289 | + const nice = key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); | |
| 290 | + const type = String(spec?.type || 'string'); | |
| 291 | + const id = `electronhub_dyn_${key.replace(/[^a-zA-Z0-9_-]/g, '_')}`; | |
| 292 | + | |
| 293 | + if (Array.isArray(spec?.enum) && spec.enum.length) { | |
| 294 | + const select = $(`<div><label for="${id}">${nice}</label><select id="${id}" class="text_pole"></select></div>`); | |
| 295 | + container.append(select); | |
| 296 | + const el = select.find('select'); | |
| 297 | + for (const opt of spec.enum) el.append(new Option(String(opt), String(opt))); | |
| 298 | + const val = this.settings[key] ?? spec.default ?? spec.enum[0]; | |
| 299 | + el.val(String(val)); | |
| 300 | + el.on('change', () => { this.settings[key] = String(el.val() || ''); saveTtsProviderSettings(); }); | |
| 301 | + continue; | |
| 302 | + } | |
| 303 | + | |
| 304 | + if (type === 'boolean') { | |
| 305 | + const block = $(`<label class="checkbox_label" for="${id}"><input type="checkbox" id="${id}"> <small>${nice}</small></label>`); | |
| 306 | + container.append(block); | |
| 307 | + const el = block.find('input'); | |
| 308 | + el.prop('checked', !!(this.settings[key] ?? spec.default ?? false)); | |
| 309 | + el.on('change', () => { this.settings[key] = !!el.is(':checked'); saveTtsProviderSettings(); }); | |
| 310 | + continue; | |
| 311 | + } | |
| 312 | + | |
| 313 | + if (type === 'number' || type === 'integer') { | |
| 314 | + const min = spec.minimum ?? undefined; | |
| 315 | + const max = spec.maximum ?? undefined; | |
| 316 | + const step = type === 'integer' ? 1 : (spec.step ?? 0.01); | |
| 317 | + const block = $(`<div><label for="${id}">${nice}${(min != null || max != null) ? ` (${min ?? ''}..${max ?? ''})` : ''}:</label><input id="${id}" type="number" class="text_pole" ${min != null ? `min="${min}"` : ''} ${max != null ? `max="${max}"` : ''} step="${step}"></div>`); | |
| 318 | + container.append(block); | |
| 319 | + const el = block.find('input'); | |
| 320 | + const val = this.settings[key] ?? spec.default ?? ''; | |
| 321 | + if (val !== '') el.val(val); | |
| 322 | + el.on('input', () => { | |
| 323 | + const raw = el.val(); | |
| 324 | + this.settings[key] = (raw === '') ? '' : Number(raw); | |
| 325 | + saveTtsProviderSettings(); | |
| 326 | + }); | |
| 327 | + continue; | |
| 328 | + } | |
| 329 | + | |
| 330 | + const isLong = /instructions|transcript|style|prompt|description/i.test(key); | |
| 331 | + if (isLong) { | |
| 332 | + const block = $(`<div><label for="${id}">${nice}</label><textarea id="${id}" class="textarea_compact autoSetHeight"></textarea></div>`); | |
| 333 | + container.append(block); | |
| 334 | + const el = block.find('textarea'); | |
| 335 | + el.val(String(this.settings[key] ?? spec.default ?? '')); | |
| 336 | + el.on('input', () => { this.settings[key] = String(el.val() || ''); saveTtsProviderSettings(); }); | |
| 337 | + } else { | |
| 338 | + const block = $(`<div><label for="${id}">${nice}</label><input id="${id}" type="text" class="text_pole" /></div>`); | |
| 339 | + container.append(block); | |
| 340 | + const el = block.find('input'); | |
| 341 | + el.val(String(this.settings[key] ?? spec.default ?? '')); | |
| 342 | + el.on('input', () => { this.settings[key] = String(el.val() || ''); saveTtsProviderSettings(); }); | |
| 343 | + } | |
| 344 | + } | |
| 345 | + } | |
| 346 | + | |
| 347 | + async checkReady() { | |
| 348 | + this.voices = await this.fetchTtsVoiceObjects(); | |
| 349 | + } | |
| 350 | + | |
| 351 | + async onRefreshClick() { | |
| 352 | + await this.loadModels(); | |
| 353 | + this.populateModelSelect(); | |
| 354 | + this.voices = await this.fetchTtsVoiceObjects(); | |
| 355 | + this.updateConditionalBlocks(); | |
| 356 | + this.renderDynamicParams(); | |
| 357 | + saveTtsProviderSettings(); | |
| 358 | + } | |
| 359 | + | |
| 360 | + async getVoice(voiceName) { | |
| 361 | + if (this.voices.length == 0) { | |
| 362 | + this.voices = await this.fetchTtsVoiceObjects(); | |
| 363 | + } | |
| 364 | + const match = this.voices.filter(v => v.name == voiceName)[0]; | |
| 365 | + if (!match) { | |
| 366 | + throw `TTS Voice name ${voiceName} not found`; | |
| 367 | + } | |
| 368 | + return match; | |
| 369 | + } | |
| 370 | + | |
| 371 | + async generateTts(text, voiceId) { | |
| 372 | + const response = await this.fetchTtsGeneration(text, voiceId); | |
| 373 | + return response; | |
| 374 | + } | |
| 375 | + | |
| 376 | + async fetchTtsVoiceObjects() { | |
| 377 | + const modelId = this.settings.model; | |
| 378 | + const model = this.models.find(m => m.id === modelId); | |
| 379 | + if (model && Array.isArray(model.voices) && model.voices.length) { | |
| 380 | + return model.voices.map(name => ({ name, voice_id: name, lang: 'en-US' })); | |
| 381 | + } | |
| 382 | + // Fallback to common OpenAI voices | |
| 383 | + const fallback = ['alloy', 'ash', 'ballad', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer', 'verse']; | |
| 384 | + return fallback.map(name => ({ name, voice_id: name, lang: 'en-US' })); | |
| 385 | + } | |
| 386 | + | |
| 387 | + async previewTtsVoice(voiceId) { | |
| 388 | + this.audioElement.pause(); | |
| 389 | + this.audioElement.currentTime = 0; | |
| 390 | + const text = getPreviewString('en-US'); | |
| 391 | + const response = await this.fetchTtsGeneration(text, voiceId); | |
| 392 | + if (!response.ok) { | |
| 393 | + throw new Error(`HTTP ${response.status}`); | |
| 394 | + } | |
| 395 | + const audio = await response.blob(); | |
| 396 | + const url = URL.createObjectURL(audio); | |
| 397 | + this.audioElement.src = url; | |
| 398 | + this.audioElement.play(); | |
| 399 | + this.audioElement.onended = () => URL.revokeObjectURL(url); | |
| 400 | + } | |
| 401 | + | |
| 402 | + async fetchTtsGeneration(inputText, voiceId) { | |
| 403 | + console.info(`Generating Electron Hub TTS for voice_id ${voiceId}`); | |
| 404 | + const body = { | |
| 405 | + input: inputText, | |
| 406 | + voice: voiceId, | |
| 407 | + speed: this.settings.speed, | |
| 408 | + temperature: this.settings.temperature, | |
| 409 | + model: this.settings.model, | |
| 410 | + }; | |
| 411 | + | |
| 412 | + const model = (this.settings.model || '').toLowerCase(); | |
| 413 | + if (model === 'gpt-4o-mini-tts') { | |
| 414 | + if (this.settings.instructions?.trim()) body.instructions = this.settings.instructions.trim(); | |
| 415 | + } | |
| 416 | + if (model.includes('dia')) { | |
| 417 | + if (this.settings.speaker_transcript?.trim()) body.speaker_transcript = this.settings.speaker_transcript.trim(); | |
| 418 | + if (Number.isFinite(this.settings.cfg_scale)) body.cfg_scale = Number(this.settings.cfg_scale); | |
| 419 | + if (Number.isFinite(this.settings.cfg_filter_top_k)) body.cfg_filter_top_k = Number(this.settings.cfg_filter_top_k); | |
| 420 | + } | |
| 421 | + if (model.includes('microsoft-tts')) { | |
| 422 | + if (Number.isFinite(this.settings.speech_rate)) body.speech_rate = Number(this.settings.speech_rate); | |
| 423 | + if (Number.isFinite(this.settings.pitch_adjustment)) body.pitch_adjustment = Number(this.settings.pitch_adjustment); | |
| 424 | + if ((this.settings.emotional_style || '').trim()) body.emotional_style = String(this.settings.emotional_style).trim(); | |
| 425 | + } | |
| 426 | + if (Number.isFinite(this.settings.top_p)) { | |
| 427 | + body.top_p = Number(this.settings.top_p); | |
| 428 | + } | |
| 429 | + | |
| 430 | + // add dynamic params based on schema | |
| 431 | + const modelObj = this.models.find(m => m.id === this.settings.model); | |
| 432 | + const params = modelObj?.parameters || {}; | |
| 433 | + const modelHasVoices = Array.isArray(modelObj?.voices) && modelObj.voices.length > 0; | |
| 434 | + const exclude = new Set(['input', 'response_format', 'model', 'speed', 'temperature', 'top_p', 'instructions', 'speaker_transcript', 'cfg_scale', 'cfg_filter_top_k', 'speech_rate', 'pitch_adjustment', 'emotional_style']); | |
| 435 | + if (modelHasVoices) exclude.add('voice'); | |
| 436 | + for (const key of Object.keys(params)) { | |
| 437 | + if (exclude.has(key)) continue; | |
| 438 | + const val = this.settings[key]; | |
| 439 | + if (val === undefined || val === '') continue; | |
| 440 | + body[key] = val; | |
| 441 | + } | |
| 442 | + | |
| 443 | + const response = await fetch('/api/openai/electronhub/generate-voice', { | |
| 444 | + method: 'POST', | |
| 445 | + headers: getRequestHeaders(), | |
| 446 | + body: JSON.stringify(body), | |
| 447 | + }); | |
| 448 | + | |
| 449 | + if (!response.ok) { | |
| 450 | + throw new Error(`HTTP ${response.status}: ${await response.text()}`); | |
| 451 | + } | |
| 452 | + | |
| 453 | + return response; | |
| 454 | + } | |
| 455 | +} | |
| @@ -33,6 +33,7 @@ import { KokoroTtsProvider } from './kokoro.js'; | ||
| 33 | 33 | import { TtsWebuiProvider } from './tts-webui.js'; |
| 34 | 34 | import { PollinationsTtsProvider } from './pollinations.js'; |
| 35 | 35 | import { MiniMaxTtsProvider } from './minimax.js'; |
| 36 | +import { ElectronHubTtsProvider } from './electronhub.js'; | |
| 36 | 37 | |
| 37 | 38 | const UPDATE_INTERVAL = 1000; |
| 38 | 39 | const wrapper = new ModuleWorkerWrapper(moduleWorker); |
| @@ -123,6 +124,7 @@ const ttsProviders = { | ||
| 123 | 124 | 'CosyVoice (Unofficial)': CosyVoiceProvider, |
| 124 | 125 | Edge: EdgeTtsProvider, |
| 125 | 126 | ElevenLabs: ElevenLabsTtsProvider, |
| 127 | + 'Electron Hub': ElectronHubTtsProvider, | |
| 126 | 128 | 'Google Translate': GoogleTranslateTtsProvider, |
| 127 | 129 | 'Google Gemini TTS': GoogleNativeTtsProvider, |
| 128 | 130 | GSVI: GSVITtsProvider, |
| @@ -276,6 +276,8 @@ export const settingsToUpdate = { | ||
| 276 | 276 | perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true], |
| 277 | 277 | groq_model: ['#model_groq_select', 'groq_model', false, true], |
| 278 | 278 | electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], |
| 279 | + electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true], | |
| 280 | + electronhub_group_models: ['#electronhub_group_models', 'electronhub_group_models', false, true], | |
| 279 | 281 | nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], |
| 280 | 282 | deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], |
| 281 | 283 | aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], |
| @@ -379,6 +381,8 @@ const default_settings = { | ||
| 379 | 381 | perplexity_model: 'sonar-pro', |
| 380 | 382 | groq_model: 'llama-3.3-70b-versatile', |
| 381 | 383 | electronhub_model: 'gpt-4o-mini', |
| 384 | + electronhub_sort_models: 'alphabetically', | |
| 385 | + electronhub_group_models: false, | |
| 382 | 386 | nanogpt_model: 'gpt-4o-mini', |
| 383 | 387 | deepseek_model: 'deepseek-chat', |
| 384 | 388 | aimlapi_model: 'gpt-4o-mini-2024-07-18', |
| @@ -473,6 +477,8 @@ const oai_settings = { | ||
| 473 | 477 | perplexity_model: 'sonar-pro', |
| 474 | 478 | groq_model: 'llama-3.1-70b-versatile', |
| 475 | 479 | electronhub_model: 'gpt-4o-mini', |
| 480 | + electronhub_sort_models: 'alphabetically', | |
| 481 | + electronhub_group_models: false, | |
| 476 | 482 | nanogpt_model: 'gpt-4o-mini', |
| 477 | 483 | deepseek_model: 'deepseek-chat', |
| 478 | 484 | aimlapi_model: 'gpt-4-turbo', |
| @@ -1710,6 +1716,59 @@ function calculateOpenRouterCost() { | ||
| 1710 | 1716 | $('#openrouter_max_prompt_cost').text(cost); |
| 1711 | 1717 | } |
| 1712 | 1718 | |
| 1719 | +function getElectronHubModelTemplate(option) { | |
| 1720 | + const model = model_list.find(x => x.id === option?.element?.value); | |
| 1721 | + | |
| 1722 | + if (!option.id || !model) { | |
| 1723 | + return option.text; | |
| 1724 | + } | |
| 1725 | + | |
| 1726 | + const inputPrice = model.pricing?.input; | |
| 1727 | + const outputPrice = model.pricing?.output; | |
| 1728 | + const price = inputPrice && outputPrice ? `$${inputPrice}/$${outputPrice} in/out Mtoken` : 'Unknown'; | |
| 1729 | + | |
| 1730 | + const visionIcon = model.metadata?.vision ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : ''; | |
| 1731 | + const reasoningIcon = model.metadata?.reasoning ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : ''; | |
| 1732 | + const toolCallsIcon = model.metadata?.function_call ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports function tools"></i>' : ''; | |
| 1733 | + const premiumIcon = model?.premium_model ? '<i class="fa-solid fa-crown fa-sm" title="This model requires a subscription"></i>' : ''; | |
| 1734 | + | |
| 1735 | + const iconsContainer = document.createElement('span'); | |
| 1736 | + iconsContainer.insertAdjacentHTML('beforeend', visionIcon); | |
| 1737 | + iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon); | |
| 1738 | + iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon); | |
| 1739 | + iconsContainer.insertAdjacentHTML('beforeend', premiumIcon); | |
| 1740 | + | |
| 1741 | + const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : ''; | |
| 1742 | + | |
| 1743 | + return $((` | |
| 1744 | + <div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}"> | |
| 1745 | + <strong>${DOMPurify.sanitize(model.name)}</strong> | ${model.tokens} ctx | <small>${price}</small>${capabilities} | |
| 1746 | + </div> | |
| 1747 | + `)); | |
| 1748 | +} | |
| 1749 | + | |
| 1750 | +function calculateElectronHubCost() { | |
| 1751 | + if (oai_settings.chat_completion_source !== chat_completion_sources.ELECTRONHUB) { | |
| 1752 | + return; | |
| 1753 | + } | |
| 1754 | + | |
| 1755 | + let cost = 'Unknown'; | |
| 1756 | + const model = model_list.find(x => x.id === oai_settings.electronhub_model); | |
| 1757 | + | |
| 1758 | + if (model?.pricing) { | |
| 1759 | + const outputCost = Number(model.pricing.output / 1000000); | |
| 1760 | + const inputCost = Number(model.pricing.input / 1000000); | |
| 1761 | + const outputTokens = oai_settings.openai_max_tokens; | |
| 1762 | + const inputTokens = (oai_settings.openai_max_context - outputTokens); | |
| 1763 | + const totalCost = (outputCost * outputTokens) + (inputCost * inputTokens); | |
| 1764 | + if (!isNaN(totalCost)) { | |
| 1765 | + cost = '$' + totalCost.toFixed(4); | |
| 1766 | + } | |
| 1767 | + } | |
| 1768 | + | |
| 1769 | + $('#electronhub_max_prompt_cost').text(cost); | |
| 1770 | +} | |
| 1771 | + | |
| 1713 | 1772 | function saveModelList(data) { |
| 1714 | 1773 | model_list = data.map((model) => ({ ...model })); |
| 1715 | 1774 | model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id)); |
| @@ -1803,16 +1862,14 @@ function saveModelList(data) { | ||
| 1803 | 1862 | } |
| 1804 | 1863 | |
| 1805 | 1864 | if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) { |
| 1865 | + model_list = model_list.filter(model => model?.endpoints?.includes('/v1/chat/completions')); | |
| 1866 | + | |
| 1867 | + model_list = electronHubSortBy(model_list, oai_settings.electronhub_sort_models); | |
| 1868 | + | |
| 1806 | 1869 | $('#model_electronhub_select').empty(); |
| 1807 | - model_list.forEach((model) => { | |
| 1870 | + | |
| 1808 | - if (model?.endpoints?.includes('/v1/chat/completions')) { | |
| 1871 | + const groupedList = oai_settings.electronhub_group_models ? electronHubGroupByVendor(model_list) : model_list; | |
| 1809 | - $('#model_electronhub_select').append( | |
| 1872 | + appendElectronHubOptions(groupedList, oai_settings.electronhub_group_models); | |
| 1810 | - $('<option>', { | |
| 1811 | - value: model.id, | |
| 1812 | - text: model.name, | |
| 1813 | - })); | |
| 1814 | - } | |
| 1815 | - }); | |
| 1816 | 1873 | |
| 1817 | 1874 | const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model); |
| 1818 | 1875 | if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) { |
| @@ -2041,6 +2098,61 @@ function openRouterGroupByVendor(array) { | ||
| 2041 | 2098 | }, new Map()); |
| 2042 | 2099 | } |
| 2043 | 2100 | |
| 2101 | +function appendElectronHubOptions(model_list, groupModels = false) { | |
| 2102 | + const appendOption = (model, parent = null) => { | |
| 2103 | + (parent || $('#model_electronhub_select')).append( | |
| 2104 | + $('<option>', { | |
| 2105 | + value: model.id, | |
| 2106 | + text: model.name, | |
| 2107 | + })); | |
| 2108 | + }; | |
| 2109 | + | |
| 2110 | + if (groupModels) { | |
| 2111 | + model_list.forEach((models, vendor) => { | |
| 2112 | + const optgroup = $('<optgroup>').attr('label', vendor); | |
| 2113 | + | |
| 2114 | + models.forEach((model) => { | |
| 2115 | + appendOption(model, optgroup); | |
| 2116 | + }); | |
| 2117 | + | |
| 2118 | + $('#model_electronhub_select').append(optgroup); | |
| 2119 | + }); | |
| 2120 | + } else { | |
| 2121 | + model_list.forEach((model) => { | |
| 2122 | + appendOption(model); | |
| 2123 | + }); | |
| 2124 | + } | |
| 2125 | + | |
| 2126 | +} | |
| 2127 | + | |
| 2128 | +function electronHubSortBy(data, property = 'alphabetically') { | |
| 2129 | + return data.sort((a, b) => { | |
| 2130 | + if (property === 'context_length') { | |
| 2131 | + return b.tokens - a.tokens; | |
| 2132 | + } else if (property === 'pricing.input') { | |
| 2133 | + return parseFloat(a.pricing.input) - parseFloat(b.pricing.input); | |
| 2134 | + } else if (property === 'pricing.output') { | |
| 2135 | + return parseFloat(a.pricing.output) - parseFloat(b.pricing.output); | |
| 2136 | + } else { | |
| 2137 | + return a?.name && b?.name && a.name.localeCompare(b.name); | |
| 2138 | + } | |
| 2139 | + }); | |
| 2140 | +} | |
| 2141 | + | |
| 2142 | +function electronHubGroupByVendor(array) { | |
| 2143 | + return array.reduce((acc, curr) => { | |
| 2144 | + const vendor = String(curr?.name || curr?.id || 'Other').split(':')[0].trim() || 'Other'; | |
| 2145 | + | |
| 2146 | + if (!acc.has(vendor)) { | |
| 2147 | + acc.set(vendor, []); | |
| 2148 | + } | |
| 2149 | + | |
| 2150 | + acc.get(vendor).push(curr); | |
| 2151 | + | |
| 2152 | + return acc; | |
| 2153 | + }, new Map()); | |
| 2154 | +} | |
| 2155 | + | |
| 2044 | 2156 | function aimlapiGroupByVendor(array) { |
| 2045 | 2157 | return array.reduce((acc, curr) => { |
| 2046 | 2158 | const vendor = curr.info.developer; |
| @@ -2191,7 +2303,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = | ||
| 2191 | 2303 | const useLogprobs = !!power_user.request_token_probabilities; |
| 2192 | 2304 | const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isAzureOpenAI || isCustom || isXAI || isAimlapi || isMoonshot); |
| 2193 | 2305 | |
| 2194 | 2306 | const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.ELECTRONHUB, chat_completion_sources.CUSTOM]; |
| 2195 | 2307 | if (oai_settings.bias_preset_selected |
| 2196 | 2308 | && logitBiasSources.includes(oai_settings.chat_completion_source) |
| 2197 | 2309 | && Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected]) |
| @@ -3523,6 +3635,8 @@ function loadOpenAISettings(data, settings) { | ||
| 3523 | 3635 | oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model; |
| 3524 | 3636 | oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model; |
| 3525 | 3637 | oai_settings.electronhub_model = settings.electronhub_model ?? default_settings.electronhub_model; |
| 3638 | + oai_settings.electronhub_sort_models = settings.electronhub_sort_models ?? default_settings.electronhub_sort_models; | |
| 3639 | + oai_settings.electronhub_group_models = settings.electronhub_group_models ?? default_settings.electronhub_group_models; | |
| 3526 | 3640 | oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model; |
| 3527 | 3641 | oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model; |
| 3528 | 3642 | oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model; |
| @@ -3666,6 +3780,8 @@ function loadOpenAISettings(data, settings) { | ||
| 3666 | 3780 | $('#openrouter_allow_fallbacks').prop('checked', oai_settings.openrouter_allow_fallbacks); |
| 3667 | 3781 | $('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change'); |
| 3668 | 3782 | $('#openrouter_middleout').val(oai_settings.openrouter_middleout); |
| 3783 | + $('#electronhub_sort_models').val(oai_settings.electronhub_sort_models); | |
| 3784 | + $('#electronhub_group_models').prop('checked', oai_settings.electronhub_group_models); | |
| 3669 | 3785 | $('#squash_system_messages').prop('checked', oai_settings.squash_system_messages); |
| 3670 | 3786 | $('#continue_prefill').prop('checked', oai_settings.continue_prefill); |
| 3671 | 3787 | $('#openai_function_calling').prop('checked', oai_settings.function_calling); |
| @@ -3928,6 +4044,8 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { | ||
| 3928 | 4044 | pollinations_model: settings.pollinations_model, |
| 3929 | 4045 | aimlapi_model: settings.aimlapi_model, |
| 3930 | 4046 | electronhub_model: settings.electronhub_model, |
| 4047 | + electronhub_sort_models: settings.electronhub_sort_models, | |
| 4048 | + electronhub_group_models: settings.electronhub_group_models, | |
| 3931 | 4049 | moonshot_model: settings.moonshot_model, |
| 3932 | 4050 | fireworks_model: settings.fireworks_model, |
| 3933 | 4051 | cometapi_model: settings.cometapi_model, |
| @@ -5093,6 +5211,8 @@ async function onModelChange() { | ||
| 5093 | 5211 | $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input'); |
| 5094 | 5212 | oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai); |
| 5095 | 5213 | $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input'); |
| 5214 | + | |
| 5215 | + calculateElectronHubCost(); | |
| 5096 | 5216 | } |
| 5097 | 5217 | |
| 5098 | 5218 | if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) { |
| @@ -5220,6 +5340,10 @@ async function onOpenrouterModelSortChange() { | ||
| 5220 | 5340 | await getStatusOpen(); |
| 5221 | 5341 | } |
| 5222 | 5342 | |
| 5343 | +async function onElectronHubModelSortChange() { | |
| 5344 | + await getStatusOpen(); | |
| 5345 | +} | |
| 5346 | + | |
| 5223 | 5347 | async function onNewPresetClick() { |
| 5224 | 5348 | const name = await Popup.show.input(t`Preset name:`, t`Hint: Use a character/group name to bind preset to a specific chat.`, oai_settings.preset_settings_openai); |
| 5225 | 5349 | |
| @@ -6117,12 +6241,14 @@ export function initOpenAI() { | ||
| 6117 | 6241 | oai_settings.openai_max_context = Number($(this).val()); |
| 6118 | 6242 | $('#openai_max_context_counter').val(`${$(this).val()}`); |
| 6119 | 6243 | calculateOpenRouterCost(); |
| 6244 | + calculateElectronHubCost(); | |
| 6120 | 6245 | saveSettingsDebounced(); |
| 6121 | 6246 | }); |
| 6122 | 6247 | |
| 6123 | 6248 | $('#openai_max_tokens').on('input', function () { |
| 6124 | 6249 | oai_settings.openai_max_tokens = Number($(this).val()); |
| 6125 | 6250 | calculateOpenRouterCost(); |
| 6251 | + calculateElectronHubCost(); | |
| 6126 | 6252 | saveSettingsDebounced(); |
| 6127 | 6253 | }); |
| 6128 | 6254 | |
| @@ -6326,6 +6452,16 @@ export function initOpenAI() { | ||
| 6326 | 6452 | saveSettingsDebounced(); |
| 6327 | 6453 | }); |
| 6328 | 6454 | |
| 6455 | + $('#electronhub_sort_models').on('input', function () { | |
| 6456 | + oai_settings.electronhub_sort_models = String($(this).val()); | |
| 6457 | + saveSettingsDebounced(); | |
| 6458 | + }); | |
| 6459 | + | |
| 6460 | + $('#electronhub_group_models').on('input', function () { | |
| 6461 | + oai_settings.electronhub_group_models = !!$(this).prop('checked'); | |
| 6462 | + saveSettingsDebounced(); | |
| 6463 | + }); | |
| 6464 | + | |
| 6329 | 6465 | $('#squash_system_messages').on('input', function () { |
| 6330 | 6466 | oai_settings.squash_system_messages = !!$(this).prop('checked'); |
| 6331 | 6467 | saveSettingsDebounced(); |
| @@ -6503,6 +6639,14 @@ export function initOpenAI() { | ||
| 6503 | 6639 | width: '100%', |
| 6504 | 6640 | templateResult: getAimlapiModelTemplate, |
| 6505 | 6641 | }); |
| 6642 | + $('#model_electronhub_select').select2({ | |
| 6643 | + placeholder: t`Select a model`, | |
| 6644 | + searchInputPlaceholder: t`Search models...`, | |
| 6645 | + searchInputCssClass: 'text_pole', | |
| 6646 | + width: '100%', | |
| 6647 | + templateResult: getElectronHubModelTemplate, | |
| 6648 | + matcher: textValueMatcher, | |
| 6649 | + }); | |
| 6506 | 6650 | $('#completion_prompt_manager_popup_entry_form_injection_trigger').select2({ |
| 6507 | 6651 | placeholder: t`All types (default)`, |
| 6508 | 6652 | width: '100%', |
| @@ -6549,6 +6693,8 @@ export function initOpenAI() { | ||
| 6549 | 6693 | $('#model_openrouter_select').on('change', onModelChange); |
| 6550 | 6694 | $('#openrouter_group_models').on('change', onOpenrouterModelSortChange); |
| 6551 | 6695 | $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange); |
| 6696 | + $('#electronhub_group_models').on('change', onElectronHubModelSortChange); | |
| 6697 | + $('#electronhub_sort_models').on('change', onElectronHubModelSortChange); | |
| 6552 | 6698 | $('#model_ai21_select').on('change', onModelChange); |
| 6553 | 6699 | $('#model_mistralai_select').on('change', onModelChange); |
| 6554 | 6700 | $('#model_cohere_select').on('change', onModelChange); |
| @@ -648,6 +648,57 @@ export function getTokenizerModel() { | ||
| 648 | 648 | } |
| 649 | 649 | } |
| 650 | 650 | |
| 651 | + if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB && oai_settings.electronhub_model) { | |
| 652 | + if (oai_settings.electronhub_model.includes('gpt-4o') || oai_settings.electronhub_model.includes('gpt-5')) { | |
| 653 | + return gpt4oTokenizer; | |
| 654 | + } | |
| 655 | + else if (oai_settings.electronhub_model.includes('gpt-4.1') || oai_settings.electronhub_model.includes('gpt-4.5')) { | |
| 656 | + return gpt4oTokenizer; | |
| 657 | + } | |
| 658 | + else if (oai_settings.electronhub_model.includes('gpt-4')) { | |
| 659 | + return gpt4Tokenizer; | |
| 660 | + } | |
| 661 | + else if (oai_settings.electronhub_model.includes('gpt-3.5-turbo')) { | |
| 662 | + return turboTokenizer; | |
| 663 | + } | |
| 664 | + else if (oai_settings.electronhub_model.includes('claude')) { | |
| 665 | + return claudeTokenizer; | |
| 666 | + } | |
| 667 | + else if (oai_settings.electronhub_model.includes('jamba')) { | |
| 668 | + return jambaTokenizer; | |
| 669 | + } | |
| 670 | + else if (oai_settings.electronhub_model.includes('deepseek') || oai_settings.electronhub_model.includes('sonar-reasoning') || oai_settings.electronhub_model.includes('r1')) { | |
| 671 | + return deepseekTokenizer; | |
| 672 | + } | |
| 673 | + else if (oai_settings.electronhub_model.includes('qwen')) { | |
| 674 | + return qwen2Tokenizer; | |
| 675 | + } | |
| 676 | + else if (oai_settings.electronhub_model.includes('gemma')) { | |
| 677 | + return gemmaTokenizer; | |
| 678 | + } | |
| 679 | + else if (oai_settings.electronhub_model.includes('mistral')) { | |
| 680 | + return mistralTokenizer; | |
| 681 | + } | |
| 682 | + else if (oai_settings.electronhub_model.includes('yi')) { | |
| 683 | + return yiTokenizer; | |
| 684 | + } | |
| 685 | + else if (oai_settings.electronhub_model.includes('llama3') || oai_settings.electronhub_model.includes('llama-3') || oai_settings.electronhub_model.startsWith('l3')) { | |
| 686 | + return llama3Tokenizer; | |
| 687 | + } | |
| 688 | + else if (oai_settings.electronhub_model.includes('llama')) { | |
| 689 | + return llamaTokenizer; | |
| 690 | + } | |
| 691 | + else if (oai_settings.electronhub_model.includes('command-a')) { | |
| 692 | + return commandATokenizer; | |
| 693 | + } | |
| 694 | + else if (oai_settings.electronhub_model.includes('command-r')) { | |
| 695 | + return commandRTokenizer; | |
| 696 | + } | |
| 697 | + else if (oai_settings.electronhub_model.includes('nemo')) { | |
| 698 | + return nemoTokenizer; | |
| 699 | + } | |
| 700 | + } | |
| 701 | + | |
| 651 | 702 | if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) { |
| 652 | 703 | if (oai_settings.cohere_model.includes('command-a')) { |
| 653 | 704 | return commandATokenizer; |
| @@ -1255,6 +1255,7 @@ async function sendElectronHubRequest(request, response) { | ||
| 1255 | 1255 | 'frequency_penalty': request.body.frequency_penalty, |
| 1256 | 1256 | 'top_p': request.body.top_p, |
| 1257 | 1257 | 'top_k': request.body.top_k, |
| 1258 | + 'logit_bias': request.body.logit_bias, | |
| 1258 | 1259 | 'seed': request.body.seed, |
| 1259 | 1260 | ...bodyParams, |
| 1260 | 1261 | }; |
| @@ -325,6 +325,104 @@ router.post('/generate-voice', async (request, response) => { | ||
| 325 | 325 | } |
| 326 | 326 | }); |
| 327 | 327 | |
| 328 | +// ElectronHub TTS proxy | |
| 329 | +router.post('/electronhub/generate-voice', async (request, response) => { | |
| 330 | + try { | |
| 331 | + const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB); | |
| 332 | + | |
| 333 | + if (!key) { | |
| 334 | + console.warn('No ElectronHub key found'); | |
| 335 | + return response.sendStatus(400); | |
| 336 | + } | |
| 337 | + | |
| 338 | + const requestBody = { | |
| 339 | + input: request.body.input, | |
| 340 | + voice: request.body.voice, | |
| 341 | + speed: request.body.speed ?? 1, | |
| 342 | + temperature: request.body.temperature ?? undefined, | |
| 343 | + model: request.body.model || 'tts-1', | |
| 344 | + response_format: 'mp3', | |
| 345 | + }; | |
| 346 | + | |
| 347 | + // Optional provider-specific params | |
| 348 | + if (request.body.instructions) requestBody.instructions = request.body.instructions; | |
| 349 | + if (request.body.speaker_transcript) requestBody.speaker_transcript = request.body.speaker_transcript; | |
| 350 | + if (Number.isFinite(request.body.cfg_scale)) requestBody.cfg_scale = Number(request.body.cfg_scale); | |
| 351 | + if (Number.isFinite(request.body.cfg_filter_top_k)) requestBody.cfg_filter_top_k = Number(request.body.cfg_filter_top_k); | |
| 352 | + if (Number.isFinite(request.body.speech_rate)) requestBody.speech_rate = Number(request.body.speech_rate); | |
| 353 | + if (Number.isFinite(request.body.pitch_adjustment)) requestBody.pitch_adjustment = Number(request.body.pitch_adjustment); | |
| 354 | + if (request.body.emotional_style) requestBody.emotional_style = request.body.emotional_style; | |
| 355 | + | |
| 356 | + // Handle dynamic parameters sent from the frontend | |
| 357 | + const knownParams = new Set(Object.keys(requestBody)); | |
| 358 | + for (const key in request.body) { | |
| 359 | + if (!knownParams.has(key) && request.body[key] !== undefined) { | |
| 360 | + requestBody[key] = request.body[key]; | |
| 361 | + } | |
| 362 | + } | |
| 363 | + | |
| 364 | + // Clean undefineds | |
| 365 | + Object.keys(requestBody).forEach(k => requestBody[k] === undefined && delete requestBody[k]); | |
| 366 | + | |
| 367 | + console.debug('ElectronHub TTS request', requestBody); | |
| 368 | + | |
| 369 | + const result = await fetch('https://api.electronhub.ai/v1/audio/speech', { | |
| 370 | + method: 'POST', | |
| 371 | + headers: { | |
| 372 | + 'Content-Type': 'application/json', | |
| 373 | + Authorization: `Bearer ${key}`, | |
| 374 | + }, | |
| 375 | + body: JSON.stringify(requestBody), | |
| 376 | + }); | |
| 377 | + | |
| 378 | + if (!result.ok) { | |
| 379 | + const text = await result.text(); | |
| 380 | + console.warn('ElectronHub TTS request failed', result.statusText, text); | |
| 381 | + return response.status(500).send(text); | |
| 382 | + } | |
| 383 | + | |
| 384 | + const contentType = result.headers.get('content-type') || 'audio/mpeg'; | |
| 385 | + const buffer = await result.arrayBuffer(); | |
| 386 | + response.setHeader('Content-Type', contentType); | |
| 387 | + return response.send(Buffer.from(buffer)); | |
| 388 | + } catch (error) { | |
| 389 | + console.error('ElectronHub TTS generation failed', error); | |
| 390 | + response.status(500).send('Internal server error'); | |
| 391 | + } | |
| 392 | +}); | |
| 393 | + | |
| 394 | +// ElectronHub model list | |
| 395 | +router.post('/electronhub/models', async (request, response) => { | |
| 396 | + try { | |
| 397 | + const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB); | |
| 398 | + | |
| 399 | + if (!key) { | |
| 400 | + console.warn('No ElectronHub key found'); | |
| 401 | + return response.sendStatus(400); | |
| 402 | + } | |
| 403 | + | |
| 404 | + const result = await fetch('https://api.electronhub.ai/v1/models', { | |
| 405 | + method: 'GET', | |
| 406 | + headers: { | |
| 407 | + Authorization: `Bearer ${key}`, | |
| 408 | + }, | |
| 409 | + }); | |
| 410 | + | |
| 411 | + if (!result.ok) { | |
| 412 | + const text = await result.text(); | |
| 413 | + console.warn('ElectronHub models request failed', result.statusText, text); | |
| 414 | + return response.status(500).send(text); | |
| 415 | + } | |
| 416 | + | |
| 417 | + const data = await result.json(); | |
| 418 | + const models = data && Array.isArray(data['data']) ? data['data'] : []; | |
| 419 | + return response.json(models); | |
| 420 | + } catch (error) { | |
| 421 | + console.error('ElectronHub models fetch failed', error); | |
| 422 | + response.status(500).send('Internal server error'); | |
| 423 | + } | |
| 424 | +}); | |
| 425 | + | |
| 328 | 426 | router.post('/generate-image', async (request, response) => { |
| 329 | 427 | try { |
| 330 | 428 | const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI); |