Chat Completion: common model sorting and grouping settings, improved NanoGPT model list (#5536) * Improve NanoGPT Model List * Update token multiplier condition to !== 1 to support showing discounts * Add Model Sorting * rework sorting to be more compact * feat: combine CC model sorting/grouping settings Co-authored-by: Copilot <copilot@github.com> * fix: adjust migration logic Co-authored-by: Copilot <copilot@github.com> * feat: implement grouping for Chutes * fix: apply review suggestions * fix: call reconnect instead of direct status check --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com>
Signed| @@ -3224,36 +3224,6 @@ | |||
| 3224 | <input id="openrouter_use_fallback" type="checkbox" /> | 3224 | <input id="openrouter_use_fallback" type="checkbox" /> |
| 3225 | <span data-i18n="Allow fallback models">Allow fallback models</span> | 3225 | <span data-i18n="Allow fallback models">Allow fallback models</span> |
| 3226 | </label> | 3226 | </label> |
| 3227 | <div class="marginTopBot5"> | ||
| 3228 | <div class="inline-drawer wide100p"> | ||
| 3229 | <div class="inline-drawer-toggle inline-drawer-header"> | ||
| 3230 | <b data-i18n="OpenRouter Model Sorting">OpenRouter Model Sorting</b> | ||
| 3231 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> | ||
| 3232 | </div> | ||
| 3233 | <div class="inline-drawer-content m-b-1"> | ||
| 3234 | <div class="marginTopBot5"> | ||
| 3235 | <label for="openrouter_sort_models" class="checkbox_label"> | ||
| 3236 | <select id="openrouter_sort_models"> | ||
| 3237 | <option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option> | ||
| 3238 | <option data-i18n="Price" value="pricing.prompt">Price (cheapest)</option> | ||
| 3239 | <option data-i18n="Context Size" value="context_length">Context Size</option> | ||
| 3240 | </select> | ||
| 3241 | </label> | ||
| 3242 | </div> | ||
| 3243 | <div class="marginTopBot5"> | ||
| 3244 | <label for="openrouter_group_models" class="checkbox_label"> | ||
| 3245 | <input id="openrouter_group_models" type="checkbox" /> | ||
| 3246 | <span data-i18n="Group by vendors">Group by vendors</span> | ||
| 3247 | </label> | ||
| 3248 | <div class="toggle-description justifyLeft wide100p"> | ||
| 3249 | <span data-i18n="Group by vendors Description"> | ||
| 3250 | Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting. | ||
| 3251 | </span> | ||
| 3252 | </div> | ||
| 3253 | </div> | ||
| 3254 | </div> | ||
| 3255 | </div> | ||
| 3256 | </div> | ||
| 3257 | <div> | 3227 | <div> |
| 3258 | <h4> | 3228 | <h4> |
| 3259 | <span data-i18n="Model Providers">Model Providers</span> | 3229 | <span data-i18n="Model Providers">Model Providers</span> |
| @@ -3641,37 +3611,6 @@ | |||
| 3641 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> | 3611 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> |
| 3642 | </select> | 3612 | </select> |
| 3643 | </div> | 3613 | </div> |
| 3644 | <div class="marginTopBot5"> | ||
| 3645 | <div class="inline-drawer wide100p"> | ||
| 3646 | <div class="inline-drawer-toggle inline-drawer-header"> | ||
| 3647 | <b data-i18n="Electron Hub Model Sorting">Electron Hub Model Sorting</b> | ||
| 3648 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> | ||
| 3649 | </div> | ||
| 3650 | <div class="inline-drawer-content m-b-1"> | ||
| 3651 | <div class="marginTopBot5"> | ||
| 3652 | <label for="electronhub_sort_models" class="checkbox_label"> | ||
| 3653 | <select id="electronhub_sort_models"> | ||
| 3654 | <option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option> | ||
| 3655 | <option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option> | ||
| 3656 | <option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option> | ||
| 3657 | <option data-i18n="Context Size" value="context_length">Context Size</option> | ||
| 3658 | </select> | ||
| 3659 | </label> | ||
| 3660 | </div> | ||
| 3661 | <div class="marginTopBot5"> | ||
| 3662 | <label for="electronhub_group_models" class="checkbox_label"> | ||
| 3663 | <input id="electronhub_group_models" type="checkbox" /> | ||
| 3664 | <span data-i18n="Group by vendors">Group by vendors</span> | ||
| 3665 | </label> | ||
| 3666 | <div class="toggle-description justifyLeft wide100p"> | ||
| 3667 | <span data-i18n="Group by vendors Description"> | ||
| 3668 | Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting. | ||
| 3669 | </span> | ||
| 3670 | </div> | ||
| 3671 | </div> | ||
| 3672 | </div> | ||
| 3673 | </div> | ||
| 3674 | </div> | ||
| 3675 | </div> | 3614 | </div> |
| 3676 | <div id="chutes_form" data-source="chutes"> | 3615 | <div id="chutes_form" data-source="chutes"> |
| 3677 | <h4 data-i18n="Chutes API Key">Chutes API Key</h4> | 3616 | <h4 data-i18n="Chutes API Key">Chutes API Key</h4> |
| @@ -3691,26 +3630,6 @@ | |||
| 3691 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> | 3630 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> |
| 3692 | </select> | 3631 | </select> |
| 3693 | </div> | 3632 | </div> |
| 3694 | <div class="marginTopBot5"> | ||
| 3695 | <div class="inline-drawer wide100p"> | ||
| 3696 | <div class="inline-drawer-toggle inline-drawer-header"> | ||
| 3697 | <b data-i18n="Chutes Model Sorting">Chutes Model Sorting</b> | ||
| 3698 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> | ||
| 3699 | </div> | ||
| 3700 | <div class="inline-drawer-content m-b-1"> | ||
| 3701 | <div class="marginTopBot5"> | ||
| 3702 | <label for="chutes_sort_models" class="checkbox_label"> | ||
| 3703 | <select id="chutes_sort_models"> | ||
| 3704 | <option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option> | ||
| 3705 | <option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option> | ||
| 3706 | <option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option> | ||
| 3707 | <option data-i18n="Context Size" value="context_length">Context Size</option> | ||
| 3708 | </select> | ||
| 3709 | </label> | ||
| 3710 | </div> | ||
| 3711 | </div> | ||
| 3712 | </div> | ||
| 3713 | </div> | ||
| 3714 | </div> | 3633 | </div> |
| 3715 | <div id="nanogpt_form" data-source="nanogpt"> | 3634 | <div id="nanogpt_form" data-source="nanogpt"> |
| 3716 | <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4> | 3635 | <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4> |
| @@ -4062,6 +3981,37 @@ | |||
| 4062 | <small data-i18n="The underlying model of your deployment. This is detected automatically when you connect.">The underlying model of your deployment. This is detected automatically when you connect.</small> | 3981 | <small data-i18n="The underlying model of your deployment. This is detected automatically when you connect.">The underlying model of your deployment. This is detected automatically when you connect.</small> |
| 4063 | </div> | 3982 | </div> |
| 4064 | </div> | 3983 | </div> |
| 3984 | <div id="model_sorting_form" data-source="openrouter,chutes,electronhub,nanogpt,aimlapi"> | ||
| 3985 | <div class="inline-drawer wide100p"> | ||
| 3986 | <div class="inline-drawer-toggle inline-drawer-header"> | ||
| 3987 | <b data-i18n="Model Sorting">Model Sorting</b> | ||
| 3988 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> | ||
| 3989 | </div> | ||
| 3990 | <div class="inline-drawer-content m-b-1"> | ||
| 3991 | <div class="marginTopBot5"> | ||
| 3992 | <label for="cc_sort_models" class="checkbox_label"> | ||
| 3993 | <select id="cc_sort_models"> | ||
| 3994 | <option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option> | ||
| 3995 | <option data-i18n="Prompt Price (cheapest)" value="pricing.prompt">Prompt Price (cheapest)</option> | ||
| 3996 | <option data-i18n="Completion Price (cheapest)" value="pricing.completion">Completion Price (cheapest)</option> | ||
| 3997 | <option data-i18n="Context Size" value="context_length">Context Size</option> | ||
| 3998 | </select> | ||
| 3999 | </label> | ||
| 4000 | </div> | ||
| 4001 | <div class="marginTopBot5"> | ||
| 4002 | <label for="cc_group_models" class="checkbox_label"> | ||
| 4003 | <input id="cc_group_models" type="checkbox" /> | ||
| 4004 | <span data-i18n="Group by vendors">Group by vendors</span> | ||
| 4005 | </label> | ||
| 4006 | <div class="toggle-description justifyLeft wide100p"> | ||
| 4007 | <span data-i18n="Group by vendors Description"> | ||
| 4008 | Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting. | ||
| 4009 | </span> | ||
| 4010 | </div> | ||
| 4011 | </div> | ||
| 4012 | </div> | ||
| 4013 | </div> | ||
| 4014 | </div> | ||
| 4065 | <div id="prompt_post_processing_form"> | 4015 | <div id="prompt_post_processing_form"> |
| 4066 | <h4> | 4016 | <h4> |
| 4067 | <span data-i18n="Prompt Post-Processing"> | 4017 | <span data-i18n="Prompt Post-Processing"> |
| @@ -306,12 +306,12 @@ export const settingsToUpdate = { | |||
| 306 | min_p: ['#min_p_openai', 'min_p_openai', false, false], | 306 | min_p: ['#min_p_openai', 'min_p_openai', false, false], |
| 307 | repetition_penalty: ['#repetition_penalty_openai', 'repetition_penalty_openai', false, false], | 307 | repetition_penalty: ['#repetition_penalty_openai', 'repetition_penalty_openai', false, false], |
| 308 | max_context_unlocked: ['#oai_max_context_unlocked', 'max_context_unlocked', true, false], | 308 | max_context_unlocked: ['#oai_max_context_unlocked', 'max_context_unlocked', true, false], |
| 309 | group_models: ['#cc_group_models', 'group_models', true, true], | ||
| 310 | sort_models: ['#cc_sort_models', 'sort_models', false, true], | ||
| 309 | openai_model: ['#model_openai_select', 'openai_model', false, true], | 311 | openai_model: ['#model_openai_select', 'openai_model', false, true], |
| 310 | claude_model: ['#model_claude_select', 'claude_model', false, true], | 312 | claude_model: ['#model_claude_select', 'claude_model', false, true], |
| 311 | openrouter_model: ['#model_openrouter_select', 'openrouter_model', false, true], | 313 | openrouter_model: ['#model_openrouter_select', 'openrouter_model', false, true], |
| 312 | openrouter_use_fallback: ['#openrouter_use_fallback', 'openrouter_use_fallback', true, true], | 314 | openrouter_use_fallback: ['#openrouter_use_fallback', 'openrouter_use_fallback', true, true], |
| 313 | openrouter_group_models: ['#openrouter_group_models', 'openrouter_group_models', false, true], | ||
| 314 | openrouter_sort_models: ['#openrouter_sort_models', 'openrouter_sort_models', false, true], | ||
| 315 | openrouter_providers: ['#openrouter_providers_chat', 'openrouter_providers', false, true], | 315 | openrouter_providers: ['#openrouter_providers_chat', 'openrouter_providers', false, true], |
| 316 | openrouter_quantizations: ['#openrouter_quantizations_chat', 'openrouter_quantizations', false, true], | 316 | openrouter_quantizations: ['#openrouter_quantizations_chat', 'openrouter_quantizations', false, true], |
| 317 | openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true, true], | 317 | openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true, true], |
| @@ -323,14 +323,11 @@ export const settingsToUpdate = { | |||
| 323 | perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true], | 323 | perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true], |
| 324 | groq_model: ['#model_groq_select', 'groq_model', false, true], | 324 | groq_model: ['#model_groq_select', 'groq_model', false, true], |
| 325 | chutes_model: ['#model_chutes_select', 'chutes_model', false, true], | 325 | chutes_model: ['#model_chutes_select', 'chutes_model', false, true], |
| 326 | chutes_sort_models: ['#chutes_sort_models', 'chutes_sort_models', false, true], | ||
| 327 | siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true], | 326 | siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true], |
| 328 | siliconflow_endpoint: ['#siliconflow_endpoint', 'siliconflow_endpoint', false, true], | 327 | siliconflow_endpoint: ['#siliconflow_endpoint', 'siliconflow_endpoint', false, true], |
| 329 | minimax_model: ['#model_minimax_select', 'minimax_model', false, true], | 328 | minimax_model: ['#model_minimax_select', 'minimax_model', false, true], |
| 330 | minimax_endpoint: ['#minimax_endpoint', 'minimax_endpoint', false, true], | 329 | minimax_endpoint: ['#minimax_endpoint', 'minimax_endpoint', false, true], |
| 331 | electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], | 330 | electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], |
| 332 | electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true], | ||
| 333 | electronhub_group_models: ['#electronhub_group_models', 'electronhub_group_models', false, true], | ||
| 334 | nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], | 331 | nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], |
| 335 | deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], | 332 | deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], |
| 336 | aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], | 333 | aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], |
| @@ -428,6 +425,8 @@ const default_settings = { | |||
| 428 | group_nudge_prompt: default_group_nudge_prompt, | 425 | group_nudge_prompt: default_group_nudge_prompt, |
| 429 | scenario_format: default_scenario_format, | 426 | scenario_format: default_scenario_format, |
| 430 | personality_format: default_personality_format, | 427 | personality_format: default_personality_format, |
| 428 | sort_models: 'alphabetically', | ||
| 429 | group_models: false, | ||
| 431 | openai_model: 'gpt-4-turbo', | 430 | openai_model: 'gpt-4-turbo', |
| 432 | claude_model: 'claude-sonnet-4-5', | 431 | claude_model: 'claude-sonnet-4-5', |
| 433 | google_model: 'gemini-2.5-pro', | 432 | google_model: 'gemini-2.5-pro', |
| @@ -438,14 +437,11 @@ const default_settings = { | |||
| 438 | perplexity_model: 'sonar-pro', | 437 | perplexity_model: 'sonar-pro', |
| 439 | groq_model: 'llama-3.3-70b-versatile', | 438 | groq_model: 'llama-3.3-70b-versatile', |
| 440 | chutes_model: 'deepseek-ai/DeepSeek-V3-0324', | 439 | chutes_model: 'deepseek-ai/DeepSeek-V3-0324', |
| 441 | chutes_sort_models: 'alphabetically', | ||
| 442 | siliconflow_model: 'deepseek-ai/DeepSeek-V3', | 440 | siliconflow_model: 'deepseek-ai/DeepSeek-V3', |
| 443 | siliconflow_endpoint: SILICONFLOW_ENDPOINT.GLOBAL, | 441 | siliconflow_endpoint: SILICONFLOW_ENDPOINT.GLOBAL, |
| 444 | minimax_model: 'MiniMax-M2.7', | 442 | minimax_model: 'MiniMax-M2.7', |
| 445 | minimax_endpoint: MINIMAX_ENDPOINT.GLOBAL, | 443 | minimax_endpoint: MINIMAX_ENDPOINT.GLOBAL, |
| 446 | electronhub_model: 'gpt-4o-mini', | 444 | electronhub_model: 'gpt-4o-mini', |
| 447 | electronhub_sort_models: 'alphabetically', | ||
| 448 | electronhub_group_models: false, | ||
| 449 | nanogpt_model: 'gpt-4o-mini', | 445 | nanogpt_model: 'gpt-4o-mini', |
| 450 | deepseek_model: 'deepseek-v4-flash', | 446 | deepseek_model: 'deepseek-v4-flash', |
| 451 | aimlapi_model: 'chatgpt-4o-latest', | 447 | aimlapi_model: 'chatgpt-4o-latest', |
| @@ -469,8 +465,6 @@ const default_settings = { | |||
| 469 | custom_include_headers: '', | 465 | custom_include_headers: '', |
| 470 | openrouter_model: openrouter_website_model, | 466 | openrouter_model: openrouter_website_model, |
| 471 | openrouter_use_fallback: false, | 467 | openrouter_use_fallback: false, |
| 472 | openrouter_group_models: false, | ||
| 473 | openrouter_sort_models: 'alphabetically', | ||
| 474 | openrouter_providers: [], | 468 | openrouter_providers: [], |
| 475 | openrouter_quantizations: [], | 469 | openrouter_quantizations: [], |
| 476 | openrouter_allow_fallbacks: true, | 470 | openrouter_allow_fallbacks: true, |
| @@ -1897,6 +1891,33 @@ function getChutesModelTemplate(option) { | |||
| 1897 | `)); | 1891 | `)); |
| 1898 | } | 1892 | } |
| 1899 | 1893 | ||
| 1894 | function calculateChutesCost() { | ||
| 1895 | if (oai_settings.chat_completion_source !== chat_completion_sources.CHUTES) { | ||
| 1896 | return; | ||
| 1897 | } | ||
| 1898 | |||
| 1899 | let cost = 'Unknown'; | ||
| 1900 | const model = model_list.find(x => x.id === oai_settings.chutes_model); | ||
| 1901 | |||
| 1902 | if (model?.pricing) { | ||
| 1903 | const outputPrice = model.pricing?.output; | ||
| 1904 | const inputPrice = model.pricing?.input; | ||
| 1905 | |||
| 1906 | if (outputPrice !== undefined && inputPrice !== undefined) { | ||
| 1907 | const outputCost = Number(outputPrice / 1000000); | ||
| 1908 | const inputCost = Number(inputPrice / 1000000); | ||
| 1909 | const outputTokens = oai_settings.openai_max_tokens; | ||
| 1910 | const inputTokens = (oai_settings.openai_max_context - outputTokens); | ||
| 1911 | const totalCost = (outputCost * outputTokens) + (inputCost * inputTokens); | ||
| 1912 | if (!isNaN(totalCost)) { | ||
| 1913 | cost = '$' + totalCost.toFixed(4); | ||
| 1914 | } | ||
| 1915 | } | ||
| 1916 | } | ||
| 1917 | |||
| 1918 | $('#chutes_max_prompt_cost').text(cost); | ||
| 1919 | } | ||
| 1920 | |||
| 1900 | function getNanoGptModelTemplate(option) { | 1921 | function getNanoGptModelTemplate(option) { |
| 1901 | const model = model_list.find(x => x.id === option?.element?.value); | 1922 | const model = model_list.find(x => x.id === option?.element?.value); |
| 1902 | 1923 | ||
| @@ -1906,10 +1927,9 @@ function getNanoGptModelTemplate(option) { | |||
| 1906 | 1927 | ||
| 1907 | const inputPrice = model.pricing?.prompt; | 1928 | const inputPrice = model.pricing?.prompt; |
| 1908 | const outputPrice = model.pricing?.completion; | 1929 | const outputPrice = model.pricing?.completion; |
| 1909 | |||
| 1910 | let price = 'Unknown'; | 1930 | let price = 'Unknown'; |
| 1931 | |||
| 1911 | if (inputPrice !== undefined && outputPrice !== undefined) { | 1932 | if (inputPrice !== undefined && outputPrice !== undefined) { |
| 1912 | // Check if both prices are 0 (free model) | ||
| 1913 | if (inputPrice === 0 && outputPrice === 0) { | 1933 | if (inputPrice === 0 && outputPrice === 0) { |
| 1914 | price = 'Free'; | 1934 | price = 'Free'; |
| 1915 | } else { | 1935 | } else { |
| @@ -1917,40 +1937,61 @@ function getNanoGptModelTemplate(option) { | |||
| 1917 | } | 1937 | } |
| 1918 | } | 1938 | } |
| 1919 | 1939 | ||
| 1940 | const visionIcon = model.capabilities?.vision ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : ''; | ||
| 1941 | const reasoningIcon = model.capabilities?.reasoning ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : ''; | ||
| 1942 | const toolCallsIcon = model.capabilities?.tool_calling ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports tool calling"></i>' : ''; | ||
| 1943 | |||
| 1944 | let subHtml = ''; | ||
| 1945 | const sub = model.subscription; | ||
| 1946 | |||
| 1947 | if (sub) { | ||
| 1948 | if (sub.included) { | ||
| 1949 | let titleText = 'Included in subscription'; | ||
| 1950 | let multiplierText = ''; | ||
| 1951 | |||
| 1952 | if (sub.inputTokenMultiplier && sub.inputTokenMultiplier !== 1) { | ||
| 1953 | multiplierText = ` (${sub.inputTokenMultiplier}x)`; | ||
| 1954 | titleText += ` - Input Multiplier: ${sub.inputTokenMultiplier}x`; | ||
| 1955 | } | ||
| 1956 | subHtml = ` <small title="${titleText}"><i class="fa-solid fa-crown fa-sm"></i> Sub${multiplierText}</small>`; | ||
| 1957 | } else if (sub.note) { | ||
| 1958 | const safeNote = DOMPurify.sanitize(sub.note); | ||
| 1959 | subHtml = ` <small title="${safeNote}"><i class="fa-solid fa-circle-info fa-sm"></i> Not in Sub</small>`; | ||
| 1960 | } | ||
| 1961 | } | ||
| 1962 | |||
| 1963 | const iconsContainer = document.createElement('span'); | ||
| 1964 | iconsContainer.insertAdjacentHTML('beforeend', visionIcon); | ||
| 1965 | iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon); | ||
| 1966 | iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon); | ||
| 1967 | iconsContainer.insertAdjacentHTML('beforeend', subHtml); | ||
| 1968 | |||
| 1969 | const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : ''; | ||
| 1970 | |||
| 1920 | const contextLength = model.context_length || 'Unknown'; | 1971 | const contextLength = model.context_length || 'Unknown'; |
| 1972 | const modelName = model.name || model.id; | ||
| 1921 | 1973 | ||
| 1922 | return $((` | 1974 | return $((` |
| 1923 | <div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}"> | 1975 | <div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}"> |
| 1924 | <strong>${DOMPurify.sanitize(model.id)}</strong> | ${contextLength} ctx | <small>${price}</small> | 1976 | <strong>${DOMPurify.sanitize(modelName)}</strong> | ${contextLength} ctx | <small>${price}</small>${capabilities} |
| 1925 | </div> | 1977 | </div> |
| 1926 | `)); | 1978 | `)); |
| 1927 | } | 1979 | } |
| 1928 | 1980 | ||
| 1929 | function calculateChutesCost() { | 1981 | function getAimlapiModelTemplate(option) { |
| 1930 | if (oai_settings.chat_completion_source !== chat_completion_sources.CHUTES) { | 1982 | const model = model_list.find(x => x.id === option?.element?.value); |
| 1931 | return; | ||
| 1932 | } | ||
| 1933 | |||
| 1934 | let cost = 'Unknown'; | ||
| 1935 | const model = model_list.find(x => x.id === oai_settings.chutes_model); | ||
| 1936 | |||
| 1937 | if (model?.pricing) { | ||
| 1938 | const outputPrice = model.pricing?.output; | ||
| 1939 | const inputPrice = model.pricing?.input; | ||
| 1940 | 1983 | ||
| 1941 | if (outputPrice !== undefined && inputPrice !== undefined) { | 1984 | if (!option.id || !model) { |
| 1942 | const outputCost = Number(outputPrice / 1000000); | 1985 | return option.text; |
| 1943 | const inputCost = Number(inputPrice / 1000000); | ||
| 1944 | const outputTokens = oai_settings.openai_max_tokens; | ||
| 1945 | const inputTokens = (oai_settings.openai_max_context - outputTokens); | ||
| 1946 | const totalCost = (outputCost * outputTokens) + (inputCost * inputTokens); | ||
| 1947 | if (!isNaN(totalCost)) { | ||
| 1948 | cost = '$' + totalCost.toFixed(4); | ||
| 1949 | } | ||
| 1950 | } | ||
| 1951 | } | 1986 | } |
| 1952 | 1987 | ||
| 1953 | $('#chutes_max_prompt_cost').text(cost); | 1988 | const vendor = model.id.split('/')[0]; |
| 1989 | |||
| 1990 | return $((` | ||
| 1991 | <div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}"> | ||
| 1992 | <div><strong>${DOMPurify.sanitize(model.info?.name || model.name || model.id)}</strong> | ${vendor}</div> | ||
| 1993 | </div> | ||
| 1994 | `)); | ||
| 1954 | } | 1995 | } |
| 1955 | 1996 | ||
| 1956 | function saveModelList(data) { | 1997 | function saveModelList(data) { |
| @@ -1958,14 +1999,22 @@ function saveModelList(data) { | |||
| 1958 | model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id)); | 1999 | model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id)); |
| 1959 | 2000 | ||
| 1960 | if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) { | 2001 | if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) { |
| 1961 | model_list = openRouterSortBy(model_list, oai_settings.openrouter_sort_models); | 2002 | model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.OPENROUTER); |
| 1962 | |||
| 1963 | $('#model_openrouter_select').empty(); | 2003 | $('#model_openrouter_select').empty(); |
| 2004 | $('#model_openrouter_select').append($('<option>', { value: openrouter_website_model, text: t`Use OpenRouter website setting` })); | ||
| 1964 | 2005 | ||
| 1965 | if (true === oai_settings.openrouter_group_models) { | 2006 | if (oai_settings.group_models) { |
| 1966 | appendOpenRouterOptions(openRouterGroupByVendor(model_list), oai_settings.openrouter_group_models); | 2007 | groupModelsByVendor(model_list, chat_completion_sources.OPENROUTER).forEach((models, vendor) => { |
| 2008 | const optgroup = $('<optgroup>').attr('label', vendor); | ||
| 2009 | models.forEach((model) => { | ||
| 2010 | optgroup.append($('<option>', { value: model.id, text: model.name })); | ||
| 2011 | }); | ||
| 2012 | $('#model_openrouter_select').append(optgroup); | ||
| 2013 | }); | ||
| 1967 | } else { | 2014 | } else { |
| 1968 | appendOpenRouterOptions(model_list); | 2015 | model_list.forEach((model) => { |
| 2016 | $('#model_openrouter_select').append($('<option>', { value: model.id, text: model.name })); | ||
| 2017 | }); | ||
| 1969 | } | 2018 | } |
| 1970 | 2019 | ||
| 1971 | $('#model_openrouter_select').val(oai_settings.openrouter_model).trigger('change'); | 2020 | $('#model_openrouter_select').val(oai_settings.openrouter_model).trigger('change'); |
| @@ -2005,13 +2054,26 @@ function saveModelList(data) { | |||
| 2005 | } | 2054 | } |
| 2006 | 2055 | ||
| 2007 | if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) { | 2056 | if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) { |
| 2057 | model_list = model_list.filter(m => m.type === 'chat-completion'); | ||
| 2058 | model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.AIMLAPI); | ||
| 2008 | $('#model_aimlapi_select').empty(); | 2059 | $('#model_aimlapi_select').empty(); |
| 2009 | const chatModels = model_list.filter(m => m.type === 'chat-completion'); | ||
| 2010 | 2060 | ||
| 2011 | appendAimlapiOptions(aimlapiGroupByVendor(chatModels)); | 2061 | if (oai_settings.group_models) { |
| 2062 | groupModelsByVendor(model_list, chat_completion_sources.AIMLAPI).forEach((models, vendor) => { | ||
| 2063 | const optgroup = $('<optgroup>').attr('label', vendor); | ||
| 2064 | models.forEach((model) => { | ||
| 2065 | optgroup.append($('<option>', { value: model.id, text: model.info?.name || model.id })); | ||
| 2066 | }); | ||
| 2067 | $('#model_aimlapi_select').append(optgroup); | ||
| 2068 | }); | ||
| 2069 | } else { | ||
| 2070 | model_list.forEach((model) => { | ||
| 2071 | $('#model_aimlapi_select').append($('<option>', { value: model.id, text: model.info?.name || model.id })); | ||
| 2072 | }); | ||
| 2073 | } | ||
| 2012 | 2074 | ||
| 2013 | if (!oai_settings.aimlapi_model && chatModels.length > 0) { | 2075 | if (!oai_settings.aimlapi_model && model_list.length > 0) { |
| 2014 | oai_settings.aimlapi_model = chatModels[0].id; | 2076 | oai_settings.aimlapi_model = model_list[0].id; |
| 2015 | } | 2077 | } |
| 2016 | 2078 | ||
| 2017 | $('#model_aimlapi_select').val(oai_settings.aimlapi_model).trigger('change'); | 2079 | $('#model_aimlapi_select').val(oai_settings.aimlapi_model).trigger('change'); |
| @@ -2034,13 +2096,22 @@ function saveModelList(data) { | |||
| 2034 | 2096 | ||
| 2035 | if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) { | 2097 | if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) { |
| 2036 | model_list = model_list.filter(model => model?.endpoints?.includes('/v1/chat/completions')); | 2098 | model_list = model_list.filter(model => model?.endpoints?.includes('/v1/chat/completions')); |
| 2037 | 2099 | model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.ELECTRONHUB); | |
| 2038 | model_list = electronHubSortBy(model_list, oai_settings.electronhub_sort_models); | ||
| 2039 | |||
| 2040 | $('#model_electronhub_select').empty(); | 2100 | $('#model_electronhub_select').empty(); |
| 2041 | 2101 | ||
| 2042 | const groupedList = oai_settings.electronhub_group_models ? electronHubGroupByVendor(model_list) : model_list; | 2102 | if (oai_settings.group_models) { |
| 2043 | appendElectronHubOptions(groupedList, oai_settings.electronhub_group_models); | 2103 | groupModelsByVendor(model_list, chat_completion_sources.ELECTRONHUB).forEach((models, vendor) => { |
| 2104 | const optgroup = $('<optgroup>').attr('label', vendor); | ||
| 2105 | models.forEach((model) => { | ||
| 2106 | optgroup.append($('<option>', { value: model.id, text: model.name })); | ||
| 2107 | }); | ||
| 2108 | $('#model_electronhub_select').append(optgroup); | ||
| 2109 | }); | ||
| 2110 | } else { | ||
| 2111 | model_list.forEach((model) => { | ||
| 2112 | $('#model_electronhub_select').append($('<option>', { value: model.id, text: model.name })); | ||
| 2113 | }); | ||
| 2114 | } | ||
| 2044 | 2115 | ||
| 2045 | const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model); | 2116 | const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model); |
| 2046 | if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) { | 2117 | if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) { |
| @@ -2052,15 +2123,21 @@ function saveModelList(data) { | |||
| 2052 | 2123 | ||
| 2053 | if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) { | 2124 | if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) { |
| 2054 | model_list = model_list.filter(model => typeof model.id === 'string' && !model.id.toLowerCase().includes('affine')); | 2125 | model_list = model_list.filter(model => typeof model.id === 'string' && !model.id.toLowerCase().includes('affine')); |
| 2055 | 2126 | model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.CHUTES); | |
| 2056 | model_list = chutesSortBy(model_list, oai_settings.chutes_sort_models); | ||
| 2057 | |||
| 2058 | $('#model_chutes_select').empty(); | 2127 | $('#model_chutes_select').empty(); |
| 2059 | 2128 | ||
| 2060 | for (const model of model_list) { | 2129 | if (oai_settings.group_models) { |
| 2061 | const option = $('<option>').val(model.id).text(model.id); | 2130 | groupModelsByVendor(model_list, chat_completion_sources.CHUTES).forEach((models, vendor) => { |
| 2062 | option.attr('data-model', JSON.stringify(model)); | 2131 | const optgroup = $('<optgroup>').attr('label', vendor); |
| 2063 | $('#model_chutes_select').append(option); | 2132 | models.forEach((model) => { |
| 2133 | optgroup.append($('<option>', { value: model.id, text: model.id })); | ||
| 2134 | }); | ||
| 2135 | $('#model_chutes_select').append(optgroup); | ||
| 2136 | }); | ||
| 2137 | } else { | ||
| 2138 | model_list.forEach((model) => { | ||
| 2139 | $('#model_chutes_select').append($('<option>', { value: model.id, text: model.id })); | ||
| 2140 | }); | ||
| 2064 | } | 2141 | } |
| 2065 | 2142 | ||
| 2066 | const selectedModel = model_list.find(model => model.id === oai_settings.chutes_model); | 2143 | const selectedModel = model_list.find(model => model.id === oai_settings.chutes_model); |
| @@ -2072,14 +2149,22 @@ function saveModelList(data) { | |||
| 2072 | } | 2149 | } |
| 2073 | 2150 | ||
| 2074 | if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) { | 2151 | if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) { |
| 2152 | model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.NANOGPT); | ||
| 2075 | $('#model_nanogpt_select').empty(); | 2153 | $('#model_nanogpt_select').empty(); |
| 2076 | model_list.forEach((model) => { | 2154 | |
| 2077 | $('#model_nanogpt_select').append( | 2155 | if (oai_settings.group_models) { |
| 2078 | $('<option>', { | 2156 | groupModelsByVendor(model_list, chat_completion_sources.NANOGPT).forEach((models, vendor) => { |
| 2079 | value: model.id, | 2157 | const optgroup = $('<optgroup>').attr('label', vendor); |
| 2080 | text: model.id, | 2158 | models.forEach((model) => { |
| 2081 | })); | 2159 | optgroup.append($('<option>', { value: model.id, text: model.name || model.id })); |
| 2082 | }); | 2160 | }); |
| 2161 | $('#model_nanogpt_select').append(optgroup); | ||
| 2162 | }); | ||
| 2163 | } else { | ||
| 2164 | model_list.forEach((model) => { | ||
| 2165 | $('#model_nanogpt_select').append($('<option>', { value: model.id, text: model.name || model.id })); | ||
| 2166 | }); | ||
| 2167 | } | ||
| 2083 | 2168 | ||
| 2084 | const selectedModel = model_list.find(model => model.id === oai_settings.nanogpt_model); | 2169 | const selectedModel = model_list.find(model => model.id === oai_settings.nanogpt_model); |
| 2085 | if (model_list.length > 0 && (!selectedModel || !oai_settings.nanogpt_model)) { | 2170 | if (model_list.length > 0 && (!selectedModel || !oai_settings.nanogpt_model)) { |
| @@ -2092,11 +2177,7 @@ function saveModelList(data) { | |||
| 2092 | if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) { | 2177 | if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) { |
| 2093 | $('#model_deepseek_select').empty(); | 2178 | $('#model_deepseek_select').empty(); |
| 2094 | model_list.forEach((model) => { | 2179 | model_list.forEach((model) => { |
| 2095 | $('#model_deepseek_select').append( | 2180 | $('#model_deepseek_select').append($('<option>', { value: model.id, text: model.id })); |
| 2096 | $('<option>', { | ||
| 2097 | value: model.id, | ||
| 2098 | text: model.id, | ||
| 2099 | })); | ||
| 2100 | }); | 2181 | }); |
| 2101 | 2182 | ||
| 2102 | const selectedModel = model_list.find(model => model.id === oai_settings.deepseek_model); | 2183 | const selectedModel = model_list.find(model => model.id === oai_settings.deepseek_model); |
| @@ -2110,11 +2191,7 @@ function saveModelList(data) { | |||
| 2110 | if (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) { | 2191 | if (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) { |
| 2111 | $('#model_pollinations_select').empty(); | 2192 | $('#model_pollinations_select').empty(); |
| 2112 | model_list.forEach((model) => { | 2193 | model_list.forEach((model) => { |
| 2113 | $('#model_pollinations_select').append( | 2194 | $('#model_pollinations_select').append($('<option>', { value: model.id, text: model.id })); |
| 2114 | $('<option>', { | ||
| 2115 | value: model.id, | ||
| 2116 | text: model.id, | ||
| 2117 | })); | ||
| 2118 | }); | 2195 | }); |
| 2119 | 2196 | ||
| 2120 | const selectedModel = model_list.find(model => model.id === oai_settings.pollinations_model); | 2197 | const selectedModel = model_list.find(model => model.id === oai_settings.pollinations_model); |
| @@ -2303,181 +2380,134 @@ function saveModelList(data) { | |||
| 2303 | } | 2380 | } |
| 2304 | } | 2381 | } |
| 2305 | 2382 | ||
| 2306 | function appendOpenRouterOptions(model_list, groupModels = false, sort = false) { | 2383 | /** |
| 2307 | $('#model_openrouter_select').append($('<option>', { value: openrouter_website_model, text: t`Use OpenRouter website setting` })); | 2384 | * Sorts models by the specified property for the given source. |
| 2308 | 2385 | * @param {object[]} data - Array of model objects | |
| 2309 | const appendOption = (model, parent = null) => { | 2386 | * @param {string} property - Sort property ('alphabetically', 'context_length', 'pricing.prompt', 'pricing.completion') |
| 2310 | (parent || $('#model_openrouter_select')).append( | 2387 | * @param {string} source - Chat Completion source (e.g., 'openrouter', 'chutes', 'electronhub', 'nanogpt') |
| 2311 | $('<option>', { | 2388 | * @returns {object[]} Sorted array of model objects |
| 2312 | value: model.id, | 2389 | */ |
| 2313 | text: model.name, | 2390 | function sortModelsBy(data, property, source) { |
| 2314 | })); | 2391 | switch (source) { |
| 2315 | }; | 2392 | case chat_completion_sources.OPENROUTER: |
| 2316 | 2393 | return data.sort((a, b) => { | |
| 2317 | if (groupModels) { | 2394 | if (property === 'context_length') { |
| 2318 | model_list.forEach((models, vendor) => { | 2395 | return (b.context_length || 0) - (a.context_length || 0); |
| 2319 | const optgroup = $(`<optgroup label="${vendor}">`); | 2396 | } else if (property === 'pricing.input' || property === 'pricing.prompt') { |
| 2320 | 2397 | return parseFloat(a.pricing?.prompt || 0) - parseFloat(b.pricing?.prompt || 0); | |
| 2321 | models.forEach((model) => { | 2398 | } else if (property === 'pricing.output' || property === 'pricing.completion') { |
| 2322 | appendOption(model, optgroup); | 2399 | return parseFloat(a.pricing?.completion || 0) - parseFloat(b.pricing?.completion || 0); |
| 2400 | } else { | ||
| 2401 | return a?.name && b?.name ? a.name.localeCompare(b.name) : 0; | ||
| 2402 | } | ||
| 2323 | }); | 2403 | }); |
| 2324 | 2404 | case chat_completion_sources.CHUTES: | |
| 2325 | $('#model_openrouter_select').append(optgroup); | 2405 | return data.sort((a, b) => { |
| 2326 | }); | 2406 | if (property === 'context_length') { |
| 2327 | } else { | 2407 | return (b.context_length || 0) - (a.context_length || 0); |
| 2328 | model_list.forEach((model) => { | 2408 | } else if (property === 'pricing.input' || property === 'pricing.prompt') { |
| 2329 | appendOption(model); | 2409 | return parseFloat(a.pricing?.input || 0) - parseFloat(b.pricing?.input || 0); |
| 2330 | }); | 2410 | } else if (property === 'pricing.output' || property === 'pricing.completion') { |
| 2331 | } | 2411 | return parseFloat(a.pricing?.output || 0) - parseFloat(b.pricing?.output || 0); |
| 2332 | } | 2412 | } else { |
| 2333 | 2413 | return a?.id && b?.id ? a.id.localeCompare(b.id) : 0; | |
| 2334 | const openRouterSortBy = (data, property = 'alphabetically') => { | 2414 | } |
| 2335 | return data.sort((a, b) => { | ||
| 2336 | if (property === 'context_length') { | ||
| 2337 | return b.context_length - a.context_length; | ||
| 2338 | } else if (property === 'pricing.prompt') { | ||
| 2339 | return parseFloat(a.pricing.prompt) - parseFloat(b.pricing.prompt); | ||
| 2340 | } else { | ||
| 2341 | // Alphabetically | ||
| 2342 | return a?.name && b?.name && a.name.localeCompare(b.name); | ||
| 2343 | } | ||
| 2344 | }); | ||
| 2345 | }; | ||
| 2346 | |||
| 2347 | function openRouterGroupByVendor(array) { | ||
| 2348 | return array.reduce((acc, curr) => { | ||
| 2349 | const vendor = curr.id.split('/')[0]; | ||
| 2350 | |||
| 2351 | if (!acc.has(vendor)) { | ||
| 2352 | acc.set(vendor, []); | ||
| 2353 | } | ||
| 2354 | |||
| 2355 | acc.get(vendor).push(curr); | ||
| 2356 | |||
| 2357 | return acc; | ||
| 2358 | }, new Map()); | ||
| 2359 | } | ||
| 2360 | |||
| 2361 | function chutesSortBy(data, property = 'alphabetically') { | ||
| 2362 | return data.sort((a, b) => { | ||
| 2363 | if (property === 'context_length') { | ||
| 2364 | return b.context_length - a.context_length; | ||
| 2365 | } else if (property === 'pricing.input') { | ||
| 2366 | const aPrice = parseFloat(a.pricing?.input || 0); | ||
| 2367 | const bPrice = parseFloat(b.pricing?.input || 0); | ||
| 2368 | return aPrice - bPrice; | ||
| 2369 | } else if (property === 'pricing.output') { | ||
| 2370 | const aPrice = parseFloat(a.pricing?.output || 0); | ||
| 2371 | const bPrice = parseFloat(b.pricing?.output || 0); | ||
| 2372 | return aPrice - bPrice; | ||
| 2373 | } else { | ||
| 2374 | return a?.id && b?.id && a.id.localeCompare(b.id); | ||
| 2375 | } | ||
| 2376 | }); | ||
| 2377 | } | ||
| 2378 | |||
| 2379 | function appendElectronHubOptions(model_list, groupModels = false) { | ||
| 2380 | const appendOption = (model, parent = null) => { | ||
| 2381 | (parent || $('#model_electronhub_select')).append( | ||
| 2382 | $('<option>', { | ||
| 2383 | value: model.id, | ||
| 2384 | text: model.name, | ||
| 2385 | })); | ||
| 2386 | }; | ||
| 2387 | |||
| 2388 | if (groupModels) { | ||
| 2389 | model_list.forEach((models, vendor) => { | ||
| 2390 | const optgroup = $('<optgroup>').attr('label', vendor); | ||
| 2391 | |||
| 2392 | models.forEach((model) => { | ||
| 2393 | appendOption(model, optgroup); | ||
| 2394 | }); | 2415 | }); |
| 2395 | 2416 | case chat_completion_sources.ELECTRONHUB: | |
| 2396 | $('#model_electronhub_select').append(optgroup); | 2417 | return data.sort((a, b) => { |
| 2397 | }); | 2418 | if (property === 'context_length') { |
| 2398 | } else { | 2419 | return (b.tokens || 0) - (a.tokens || 0); |
| 2399 | model_list.forEach((model) => { | 2420 | } else if (property === 'pricing.input' || property === 'pricing.prompt') { |
| 2400 | appendOption(model); | 2421 | return parseFloat(a.pricing?.input || 0) - parseFloat(b.pricing?.input || 0); |
| 2401 | }); | 2422 | } else if (property === 'pricing.output' || property === 'pricing.completion') { |
| 2423 | return parseFloat(a.pricing?.output || 0) - parseFloat(b.pricing?.output || 0); | ||
| 2424 | } else { | ||
| 2425 | return a?.name && b?.name ? a.name.localeCompare(b.name) : 0; | ||
| 2426 | } | ||
| 2427 | }); | ||
| 2428 | case chat_completion_sources.NANOGPT: | ||
| 2429 | return data.sort((a, b) => { | ||
| 2430 | if (property === 'context_length') { | ||
| 2431 | return (b.context_length || 0) - (a.context_length || 0); | ||
| 2432 | } else if (property === 'pricing.input' || property === 'pricing.prompt') { | ||
| 2433 | return parseFloat(a.pricing?.prompt || 0) - parseFloat(b.pricing?.prompt || 0); | ||
| 2434 | } else if (property === 'pricing.output' || property === 'pricing.completion') { | ||
| 2435 | return parseFloat(a.pricing?.completion || 0) - parseFloat(b.pricing?.completion || 0); | ||
| 2436 | } else { | ||
| 2437 | return a?.name && b?.name ? a.name.localeCompare(b.name) : 0; | ||
| 2438 | } | ||
| 2439 | }); | ||
| 2440 | case chat_completion_sources.AIMLAPI: | ||
| 2441 | return data.sort((a, b) => { | ||
| 2442 | if (property === 'context_length') { | ||
| 2443 | return (b.info?.contextLength || 0) - (a.info?.contextLength || 0); | ||
| 2444 | } else { | ||
| 2445 | // No pricing information on the API. Sort alphabetically by name. | ||
| 2446 | return a?.info?.name && b?.info?.name ? a.info.name.localeCompare(b.info.name) : 0; | ||
| 2447 | } | ||
| 2448 | }); | ||
| 2449 | default: | ||
| 2450 | return data; | ||
| 2402 | } | 2451 | } |
| 2403 | } | 2452 | } |
| 2404 | 2453 | ||
| 2405 | function electronHubSortBy(data, property = 'alphabetically') { | 2454 | /** |
| 2406 | return data.sort((a, b) => { | 2455 | * Groups models by vendor for the given source. If not supported, returns a map with a single entry containing all models. |
| 2407 | if (property === 'context_length') { | 2456 | * @param {object[]} array Array of model objects |
| 2408 | return b.tokens - a.tokens; | 2457 | * @param {string} source Chat Completion source (e.g., 'openrouter') |
| 2409 | } else if (property === 'pricing.input') { | 2458 | * @returns {Map<string, object[]>} Map of vendor to array of models |
| 2410 | return parseFloat(a.pricing.input) - parseFloat(b.pricing.input); | 2459 | */ |
| 2411 | } else if (property === 'pricing.output') { | 2460 | function groupModelsByVendor(array, source) { |
| 2412 | return parseFloat(a.pricing.output) - parseFloat(b.pricing.output); | 2461 | switch (source) { |
| 2413 | } else { | 2462 | case chat_completion_sources.OPENROUTER: |
| 2414 | return a?.name && b?.name && a.name.localeCompare(b.name); | 2463 | return array.reduce((acc, curr) => { |
| 2415 | } | 2464 | const vendor = curr.id.split('/')[0]; |
| 2416 | }); | 2465 | if (!acc.has(vendor)) { |
| 2417 | } | 2466 | acc.set(vendor, []); |
| 2418 | 2467 | } | |
| 2419 | function electronHubGroupByVendor(array) { | 2468 | acc.get(vendor).push(curr); |
| 2420 | return array.reduce((acc, curr) => { | 2469 | return acc; |
| 2421 | const vendor = String(curr?.name || curr?.id || 'Other').split(':')[0].trim() || 'Other'; | 2470 | }, new Map()); |
| 2422 | 2471 | case chat_completion_sources.ELECTRONHUB: | |
| 2423 | if (!acc.has(vendor)) { | 2472 | return array.reduce((acc, curr) => { |
| 2424 | acc.set(vendor, []); | 2473 | const vendor = String(curr?.name || curr?.id || 'Other').split(':')[0].trim() || 'Other'; |
| 2425 | } | 2474 | if (!acc.has(vendor)) { |
| 2426 | 2475 | acc.set(vendor, []); | |
| 2427 | acc.get(vendor).push(curr); | 2476 | } |
| 2428 | 2477 | acc.get(vendor).push(curr); | |
| 2429 | return acc; | 2478 | return acc; |
| 2430 | }, new Map()); | 2479 | }, new Map()); |
| 2431 | } | 2480 | case chat_completion_sources.NANOGPT: |
| 2432 | 2481 | return array.reduce((acc, curr) => { | |
| 2433 | function aimlapiGroupByVendor(array) { | 2482 | const vendorPart = /\//.test(curr.id) ? curr.id.split('/')[0] : curr.id.split('-')[0]; |
| 2434 | return array.reduce((acc, curr) => { | 2483 | const vendor = String(vendorPart?.trim()?.toLowerCase() || 'Other'); |
| 2435 | const vendor = curr.info.developer; | 2484 | if (!acc.has(vendor)) { |
| 2436 | 2485 | acc.set(vendor, []); | |
| 2437 | if (!acc.has(vendor)) { | 2486 | } |
| 2438 | acc.set(vendor, []); | 2487 | acc.get(vendor).push(curr); |
| 2439 | } | 2488 | return acc; |
| 2440 | 2489 | }, new Map()); | |
| 2441 | acc.get(vendor).push(curr); | 2490 | case chat_completion_sources.CHUTES: |
| 2442 | 2491 | return array.reduce((acc, curr) => { | |
| 2443 | return acc; | 2492 | const vendor = curr.id.split('/')[0]; |
| 2444 | }, new Map()); | 2493 | if (!acc.has(vendor)) { |
| 2445 | } | 2494 | acc.set(vendor, []); |
| 2446 | 2495 | } | |
| 2447 | function appendAimlapiOptions(model_list) { | 2496 | acc.get(vendor).push(curr); |
| 2448 | const appendOption = (model, parent = null) => { | 2497 | return acc; |
| 2449 | (parent || $('#model_aimlapi_select')).append( | 2498 | }, new Map()); |
| 2450 | $('<option>', { | 2499 | case chat_completion_sources.AIMLAPI: |
| 2451 | value: model.id, | 2500 | return array.reduce((acc, curr) => { |
| 2452 | text: model.info?.name || model.name || model.id, | 2501 | const vendor = curr.info?.developer || 'Other'; |
| 2453 | })); | 2502 | if (!acc.has(vendor)) { |
| 2454 | }; | 2503 | acc.set(vendor, []); |
| 2455 | 2504 | } | |
| 2456 | model_list.forEach((models, vendor) => { | 2505 | acc.get(vendor).push(curr); |
| 2457 | const optgroup = $(`<optgroup label="${vendor}">`); | 2506 | return acc; |
| 2458 | 2507 | }, new Map()); | |
| 2459 | models.forEach((model) => { | 2508 | default: |
| 2460 | appendOption(model, optgroup); | 2509 | return new Map([['', array]]); |
| 2461 | }); | ||
| 2462 | |||
| 2463 | $('#model_aimlapi_select').append(optgroup); | ||
| 2464 | }); | ||
| 2465 | } | ||
| 2466 | |||
| 2467 | function getAimlapiModelTemplate(option) { | ||
| 2468 | const model = model_list.find(x => x.id === option?.element?.value); | ||
| 2469 | |||
| 2470 | if (!option.id || !model) { | ||
| 2471 | return option.text; | ||
| 2472 | } | 2510 | } |
| 2473 | |||
| 2474 | const vendor = model.id.split('/')[0]; | ||
| 2475 | |||
| 2476 | return $((` | ||
| 2477 | <div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}"> | ||
| 2478 | <div><strong>${DOMPurify.sanitize(model.info?.name || model.name || model.id)}</strong> | ${vendor}</div> | ||
| 2479 | </div> | ||
| 2480 | `)); | ||
| 2481 | } | 2511 | } |
| 2482 | 2512 | ||
| 2483 | /** | 2513 | /** |
| @@ -4151,6 +4181,10 @@ function migrateChatCompletionSettings(settings) { | |||
| 4151 | { oldKey: 'use_makersuite_sysprompt', oldValue: true, newKey: 'use_sysprompt', newValue: true }, | 4181 | { oldKey: 'use_makersuite_sysprompt', oldValue: true, newKey: 'use_sysprompt', newValue: true }, |
| 4152 | { oldKey: 'mistralai_model', oldValue: /^(mistral-medium|mistral-small)$/, newKey: 'mistralai_model', newValue: (settings.mistralai_model + '-latest') }, | 4182 | { oldKey: 'mistralai_model', oldValue: /^(mistral-medium|mistral-small)$/, newKey: 'mistralai_model', newValue: (settings.mistralai_model + '-latest') }, |
| 4153 | { oldKey: 'deepseek_model', oldValue: /^deepseek-(chat|reasoner|coder)$/, newKey: 'deepseek_model', newValue: 'deepseek-v4-flash' }, | 4183 | { oldKey: 'deepseek_model', oldValue: /^deepseek-(chat|reasoner|coder)$/, newKey: 'deepseek_model', newValue: 'deepseek-v4-flash' }, |
| 4184 | { oldKey: 'openrouter_sort_models', oldValue: 'alphabetically', newKey: 'sort_models', newValue: 'alphabetically' }, | ||
| 4185 | { oldKey: 'openrouter_sort_models', oldValue: 'pricing.prompt', newKey: 'sort_models', newValue: 'pricing.prompt' }, | ||
| 4186 | { oldKey: 'openrouter_sort_models', oldValue: 'context_length', newKey: 'sort_models', newValue: 'context_length' }, | ||
| 4187 | { oldKey: 'openrouter_group_models', oldValue: true, newKey: 'group_models', newValue: true }, | ||
| 4154 | ]; | 4188 | ]; |
| 4155 | 4189 | ||
| 4156 | for (const migration of migrateMap) { | 4190 | for (const migration of migrateMap) { |
| @@ -5845,18 +5879,6 @@ async function onModelChange() { | |||
| 5845 | eventSource.emit(event_types.CHATCOMPLETION_MODEL_CHANGED, value); | 5879 | eventSource.emit(event_types.CHATCOMPLETION_MODEL_CHANGED, value); |
| 5846 | } | 5880 | } |
| 5847 | 5881 | ||
| 5848 | async function onOpenrouterModelSortChange() { | ||
| 5849 | await getStatusOpen(); | ||
| 5850 | } | ||
| 5851 | |||
| 5852 | async function onChutesModelSortChange() { | ||
| 5853 | await getStatusOpen(); | ||
| 5854 | } | ||
| 5855 | |||
| 5856 | async function onElectronHubModelSortChange() { | ||
| 5857 | await getStatusOpen(); | ||
| 5858 | } | ||
| 5859 | |||
| 5860 | async function onNewPresetClick() { | 5882 | async function onNewPresetClick() { |
| 5861 | 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); | 5883 | 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); |
| 5862 | 5884 | ||
| @@ -6841,16 +6863,6 @@ export function initOpenAI() { | |||
| 6841 | saveSettingsDebounced(); | 6863 | saveSettingsDebounced(); |
| 6842 | }); | 6864 | }); |
| 6843 | 6865 | ||
| 6844 | $('#openrouter_group_models').on('input', function () { | ||
| 6845 | oai_settings.openrouter_group_models = !!$(this).prop('checked'); | ||
| 6846 | saveSettingsDebounced(); | ||
| 6847 | }); | ||
| 6848 | |||
| 6849 | $('#openrouter_sort_models').on('input', function () { | ||
| 6850 | oai_settings.openrouter_sort_models = String($(this).val()); | ||
| 6851 | saveSettingsDebounced(); | ||
| 6852 | }); | ||
| 6853 | |||
| 6854 | $('#openrouter_allow_fallbacks').on('input', function () { | 6866 | $('#openrouter_allow_fallbacks').on('input', function () { |
| 6855 | oai_settings.openrouter_allow_fallbacks = !!$(this).prop('checked'); | 6867 | oai_settings.openrouter_allow_fallbacks = !!$(this).prop('checked'); |
| 6856 | updateOpenRouterProvidersWarning('#openrouter_providers_chat'); | 6868 | updateOpenRouterProvidersWarning('#openrouter_providers_chat'); |
| @@ -6862,21 +6874,6 @@ export function initOpenAI() { | |||
| 6862 | saveSettingsDebounced(); | 6874 | saveSettingsDebounced(); |
| 6863 | }); | 6875 | }); |
| 6864 | 6876 | ||
| 6865 | $('#electronhub_sort_models').on('input', function () { | ||
| 6866 | oai_settings.electronhub_sort_models = String($(this).val()); | ||
| 6867 | saveSettingsDebounced(); | ||
| 6868 | }); | ||
| 6869 | |||
| 6870 | $('#chutes_sort_models').on('input', function () { | ||
| 6871 | oai_settings.chutes_sort_models = String($(this).val()); | ||
| 6872 | saveSettingsDebounced(); | ||
| 6873 | }); | ||
| 6874 | |||
| 6875 | $('#electronhub_group_models').on('input', function () { | ||
| 6876 | oai_settings.electronhub_group_models = !!$(this).prop('checked'); | ||
| 6877 | saveSettingsDebounced(); | ||
| 6878 | }); | ||
| 6879 | |||
| 6880 | $('#squash_system_messages').on('input', function () { | 6877 | $('#squash_system_messages').on('input', function () { |
| 6881 | oai_settings.squash_system_messages = !!$(this).prop('checked'); | 6878 | oai_settings.squash_system_messages = !!$(this).prop('checked'); |
| 6882 | saveSettingsDebounced(); | 6879 | saveSettingsDebounced(); |
| @@ -7142,6 +7139,18 @@ export function initOpenAI() { | |||
| 7142 | saveSettingsDebounced(); | 7139 | saveSettingsDebounced(); |
| 7143 | }); | 7140 | }); |
| 7144 | 7141 | ||
| 7142 | $('#cc_group_models').on('input', async () => { | ||
| 7143 | oai_settings.group_models = $('#cc_group_models').prop('checked'); | ||
| 7144 | reconnectOpenAi(); | ||
| 7145 | saveSettingsDebounced(); | ||
| 7146 | }); | ||
| 7147 | |||
| 7148 | $('#cc_sort_models').on('input', async () => { | ||
| 7149 | oai_settings.sort_models = $('#cc_sort_models').val().toString(); | ||
| 7150 | reconnectOpenAi(); | ||
| 7151 | saveSettingsDebounced(); | ||
| 7152 | }); | ||
| 7153 | |||
| 7145 | $('#api_button_openai').on('click', onConnectButtonClick); | 7154 | $('#api_button_openai').on('click', onConnectButtonClick); |
| 7146 | $('#openai_reverse_proxy').on('input', onReverseProxyInput); | 7155 | $('#openai_reverse_proxy').on('input', onReverseProxyInput); |
| 7147 | $('#model_openai_select').on('change', onModelChange); | 7156 | $('#model_openai_select').on('change', onModelChange); |
| @@ -7177,11 +7186,6 @@ export function initOpenAI() { | |||
| 7177 | $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount); | 7186 | $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount); |
| 7178 | $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount); | 7187 | $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount); |
| 7179 | $('#model_openrouter_select').on('change', onModelChange); | 7188 | $('#model_openrouter_select').on('change', onModelChange); |
| 7180 | $('#openrouter_group_models').on('change', onOpenrouterModelSortChange); | ||
| 7181 | $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange); | ||
| 7182 | $('#chutes_sort_models').on('change', onChutesModelSortChange); | ||
| 7183 | $('#electronhub_group_models').on('change', onElectronHubModelSortChange); | ||
| 7184 | $('#electronhub_sort_models').on('change', onElectronHubModelSortChange); | ||
| 7185 | $('#model_ai21_select').on('change', onModelChange); | 7189 | $('#model_ai21_select').on('change', onModelChange); |
| 7186 | $('#model_mistralai_select').on('change', onModelChange); | 7190 | $('#model_mistralai_select').on('change', onModelChange); |
| 7187 | $('#model_cohere_select').on('change', onModelChange); | 7191 | $('#model_cohere_select').on('change', onModelChange); |