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>

d327412e29847d5c38d6a66e5d48b03c22809b90

DeathStalker471 <thomasnealson85@gmail.com>

Signed
2 files changed, +319 -365Ignore whitespace
public/index.html+31 -81
@@ -3224,36 +3224,6 @@
32243224 <input id="openrouter_use_fallback" type="checkbox" />
32253225 <span data-i18n="Allow fallback models">Allow fallback models</span>
32263226 </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>
32573227 <div>
32583228 <h4>
32593229 <span data-i18n="Model Providers">Model Providers</span>
@@ -3641,37 +3611,6 @@
36413611 <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
36423612 </select>
36433613 </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>
36753614 </div>
36763615 <div id="chutes_form" data-source="chutes">
36773616 <h4 data-i18n="Chutes API Key">Chutes API Key</h4>
@@ -3691,26 +3630,6 @@
36913630 <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
36923631 </select>
36933632 </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>
37143633 </div>
37153634 <div id="nanogpt_form" data-source="nanogpt">
37163635 <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4>
@@ -4062,6 +3981,37 @@
40623981 <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>
40633982 </div>
40643983 </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>
40654015 <div id="prompt_post_processing_form">
40664016 <h4>
40674017 <span data-i18n="Prompt Post-Processing">
public/scripts/openai.js+288 -284
@@ -306,12 +306,12 @@ export const settingsToUpdate = {
306306 min_p: ['#min_p_openai', 'min_p_openai', false, false],
307307 repetition_penalty: ['#repetition_penalty_openai', 'repetition_penalty_openai', false, false],
308308 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],
309311 openai_model: ['#model_openai_select', 'openai_model', false, true],
310312 claude_model: ['#model_claude_select', 'claude_model', false, true],
311313 openrouter_model: ['#model_openrouter_select', 'openrouter_model', false, true],
312314 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],
315315 openrouter_providers: ['#openrouter_providers_chat', 'openrouter_providers', false, true],
316316 openrouter_quantizations: ['#openrouter_quantizations_chat', 'openrouter_quantizations', false, true],
317317 openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true, true],
@@ -323,14 +323,11 @@ export const settingsToUpdate = {
323323 perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true],
324324 groq_model: ['#model_groq_select', 'groq_model', false, true],
325325 chutes_model: ['#model_chutes_select', 'chutes_model', false, true],
326- chutes_sort_models: ['#chutes_sort_models', 'chutes_sort_models', false, true],
327326 siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true],
328327 siliconflow_endpoint: ['#siliconflow_endpoint', 'siliconflow_endpoint', false, true],
329328 minimax_model: ['#model_minimax_select', 'minimax_model', false, true],
330329 minimax_endpoint: ['#minimax_endpoint', 'minimax_endpoint', false, true],
331330 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],
334331 nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true],
335332 deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true],
336333 aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true],
@@ -428,6 +425,8 @@ const default_settings = {
428425 group_nudge_prompt: default_group_nudge_prompt,
429426 scenario_format: default_scenario_format,
430427 personality_format: default_personality_format,
428+ sort_models: 'alphabetically',
429+ group_models: false,
431430 openai_model: 'gpt-4-turbo',
432431 claude_model: 'claude-sonnet-4-5',
433432 google_model: 'gemini-2.5-pro',
@@ -438,14 +437,11 @@ const default_settings = {
438437 perplexity_model: 'sonar-pro',
439438 groq_model: 'llama-3.3-70b-versatile',
440439 chutes_model: 'deepseek-ai/DeepSeek-V3-0324',
441- chutes_sort_models: 'alphabetically',
442440 siliconflow_model: 'deepseek-ai/DeepSeek-V3',
443441 siliconflow_endpoint: SILICONFLOW_ENDPOINT.GLOBAL,
444442 minimax_model: 'MiniMax-M2.7',
445443 minimax_endpoint: MINIMAX_ENDPOINT.GLOBAL,
446444 electronhub_model: 'gpt-4o-mini',
447- electronhub_sort_models: 'alphabetically',
448- electronhub_group_models: false,
449445 nanogpt_model: 'gpt-4o-mini',
450446 deepseek_model: 'deepseek-v4-flash',
451447 aimlapi_model: 'chatgpt-4o-latest',
@@ -469,8 +465,6 @@ const default_settings = {
469465 custom_include_headers: '',
470466 openrouter_model: openrouter_website_model,
471467 openrouter_use_fallback: false,
472- openrouter_group_models: false,
473- openrouter_sort_models: 'alphabetically',
474468 openrouter_providers: [],
475469 openrouter_quantizations: [],
476470 openrouter_allow_fallbacks: true,
@@ -1897,6 +1891,33 @@ function getChutesModelTemplate(option) {
18971891 `));
18981892}
18991893
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+
19001921function getNanoGptModelTemplate(option) {
19011922 const model = model_list.find(x => x.id === option?.element?.value);
19021923
@@ -1906,10 +1927,9 @@ function getNanoGptModelTemplate(option) {
19061927
19071928 const inputPrice = model.pricing?.prompt;
19081929 const outputPrice = model.pricing?.completion;
1909-
19101930 let price = 'Unknown';
1931+
19111932 if (inputPrice !== undefined && outputPrice !== undefined) {
1912- // Check if both prices are 0 (free model)
19131933 if (inputPrice === 0 && outputPrice === 0) {
19141934 price = 'Free';
19151935 } else {
@@ -1917,40 +1937,61 @@ function getNanoGptModelTemplate(option) {
19171937 }
19181938 }
19191939
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+
19201971 const contextLength = model.context_length || 'Unknown';
1972+ const modelName = model.name || model.id;
19211973
19221974 return $((`
19231975 <div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}">
19241976 <strong>${DOMPurify.sanitize(model.idmodelName)}</strong> | ${contextLength} ctx | <small>${price}</small>${capabilities}
19251977 </div>
19261978 `));
19271979}
19281980
19291981function calculateChutesCostgetAimlapiModelTemplate(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;
19401983
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- }
19511986 }
19521987
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+ `));
19541995}
19551996
19561997function saveModelList(data) {
@@ -1958,14 +1999,22 @@ function saveModelList(data) {
19581999 model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id));
19592000
19602001 if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
19612002 model_list = openRouterSortBysortModelsBy(model_list, oai_settings.openrouter_sort_modelssort_models, chat_completion_sources.OPENROUTER);
1962-
19632003 $('#model_openrouter_select').empty();
2004+ $('#model_openrouter_select').append($('<option>', { value: openrouter_website_model, text: t`Use OpenRouter website setting` }));
19642005
19652006 if (true === oai_settings.openrouter_group_modelsgroup_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+ });
19672014 } else {
1968- appendOpenRouterOptions(model_list);
2015+ model_list.forEach((model) => {
2016+ $('#model_openrouter_select').append($('<option>', { value: model.id, text: model.name }));
2017+ });
19692018 }
19702019
19712020 $('#model_openrouter_select').val(oai_settings.openrouter_model).trigger('change');
@@ -2005,13 +2054,26 @@ function saveModelList(data) {
20052054 }
20062055
20072056 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);
20082059 $('#model_aimlapi_select').empty();
2009- const chatModels = model_list.filter(m => m.type === 'chat-completion');
20102060
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+ }
20122074
20132075 if (!oai_settings.aimlapi_model && chatModelsmodel_list.length > 0) {
20142076 oai_settings.aimlapi_model = chatModelsmodel_list[0].id;
20152077 }
20162078
20172079 $('#model_aimlapi_select').val(oai_settings.aimlapi_model).trigger('change');
@@ -2034,13 +2096,22 @@ function saveModelList(data) {
20342096
20352097 if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
20362098 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-
20402100 $('#model_electronhub_select').empty();
20412101
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+ }
20442115
20452116 const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model);
20462117 if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) {
@@ -2052,15 +2123,21 @@ function saveModelList(data) {
20522123
20532124 if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
20542125 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-
20582127 $('#model_chutes_select').empty();
20592128
20602129 forif (const model of model_listoai_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+ });
20642141 }
20652142
20662143 const selectedModel = model_list.find(model => model.id === oai_settings.chutes_model);
@@ -2072,14 +2149,22 @@ function saveModelList(data) {
20722149 }
20732150
20742151 if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
2152+ model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.NANOGPT);
20752153 $('#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 }));
20822160 });
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+ }
20832168
20842169 const selectedModel = model_list.find(model => model.id === oai_settings.nanogpt_model);
20852170 if (model_list.length > 0 && (!selectedModel || !oai_settings.nanogpt_model)) {
@@ -2092,11 +2177,7 @@ function saveModelList(data) {
20922177 if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
20932178 $('#model_deepseek_select').empty();
20942179 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- }));
21002181 });
21012182
21022183 const selectedModel = model_list.find(model => model.id === oai_settings.deepseek_model);
@@ -2110,11 +2191,7 @@ function saveModelList(data) {
21102191 if (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) {
21112192 $('#model_pollinations_select').empty();
21122193 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- }));
21182195 });
21192196
21202197 const selectedModel = model_list.find(model => model.id === oai_settings.pollinations_model);
@@ -2303,181 +2380,134 @@ function saveModelList(data) {
23032380 }
23042381}
23052382
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) => {
23172394 if (groupModelsproperty === '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+ }
23232403 });
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);
23942415 });
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;
24022451 }
24032452}
24042453
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);
24292478 return acc;
24302479 }, 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;
24722510 }
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- `));
24812511}
24822512
24832513/**
@@ -4151,6 +4181,10 @@ function migrateChatCompletionSettings(settings) {
41514181 { oldKey: 'use_makersuite_sysprompt', oldValue: true, newKey: 'use_sysprompt', newValue: true },
41524182 { oldKey: 'mistralai_model', oldValue: /^(mistral-medium|mistral-small)$/, newKey: 'mistralai_model', newValue: (settings.mistralai_model + '-latest') },
41534183 { 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 },
41544188 ];
41554189
41564190 for (const migration of migrateMap) {
@@ -5845,18 +5879,6 @@ async function onModelChange() {
58455879 eventSource.emit(event_types.CHATCOMPLETION_MODEL_CHANGED, value);
58465880}
58475881
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-
58605882async function onNewPresetClick() {
58615883 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);
58625884
@@ -6841,16 +6863,6 @@ export function initOpenAI() {
68416863 saveSettingsDebounced();
68426864 });
68436865
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-
68546866 $('#openrouter_allow_fallbacks').on('input', function () {
68556867 oai_settings.openrouter_allow_fallbacks = !!$(this).prop('checked');
68566868 updateOpenRouterProvidersWarning('#openrouter_providers_chat');
@@ -6862,21 +6874,6 @@ export function initOpenAI() {
68626874 saveSettingsDebounced();
68636875 });
68646876
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-
68806877 $('#squash_system_messages').on('input', function () {
68816878 oai_settings.squash_system_messages = !!$(this).prop('checked');
68826879 saveSettingsDebounced();
@@ -7142,6 +7139,18 @@ export function initOpenAI() {
71427139 saveSettingsDebounced();
71437140 });
71447141
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+
71457154 $('#api_button_openai').on('click', onConnectButtonClick);
71467155 $('#openai_reverse_proxy').on('input', onReverseProxyInput);
71477156 $('#model_openai_select').on('change', onModelChange);
@@ -7177,11 +7186,6 @@ export function initOpenAI() {
71777186 $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount);
71787187 $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount);
71797188 $('#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);
71857189 $('#model_ai21_select').on('change', onModelChange);
71867190 $('#model_mistralai_select').on('change', onModelChange);
71877191 $('#model_cohere_select').on('change', onModelChange);