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>
This commit is contained in:
DeathStalker471
2026-04-26 13:25:56 -07:00
committed by GitHub
parent 97dba399e4
commit d327412e29
2 changed files with 323 additions and 369 deletions
+31 -81
View File
@@ -3224,36 +3224,6 @@
<input id="openrouter_use_fallback" type="checkbox" />
<span data-i18n="Allow fallback models">Allow fallback models</span>
</label>
<div class="marginTopBot5">
<div class="inline-drawer wide100p">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="OpenRouter Model Sorting">OpenRouter Model Sorting</b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content m-b-1">
<div class="marginTopBot5">
<label for="openrouter_sort_models" class="checkbox_label">
<select id="openrouter_sort_models">
<option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option>
<option data-i18n="Price" value="pricing.prompt">Price (cheapest)</option>
<option data-i18n="Context Size" value="context_length">Context Size</option>
</select>
</label>
</div>
<div class="marginTopBot5">
<label for="openrouter_group_models" class="checkbox_label">
<input id="openrouter_group_models" type="checkbox" />
<span data-i18n="Group by vendors">Group by vendors</span>
</label>
<div class="toggle-description justifyLeft wide100p">
<span data-i18n="Group by vendors Description">
Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting.
</span>
</div>
</div>
</div>
</div>
</div>
<div>
<h4>
<span data-i18n="Model Providers">Model Providers</span>
@@ -3641,37 +3611,6 @@
<option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
</select>
</div>
<div class="marginTopBot5">
<div class="inline-drawer wide100p">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="Electron Hub Model Sorting">Electron Hub Model Sorting</b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content m-b-1">
<div class="marginTopBot5">
<label for="electronhub_sort_models" class="checkbox_label">
<select id="electronhub_sort_models">
<option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option>
<option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option>
<option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option>
<option data-i18n="Context Size" value="context_length">Context Size</option>
</select>
</label>
</div>
<div class="marginTopBot5">
<label for="electronhub_group_models" class="checkbox_label">
<input id="electronhub_group_models" type="checkbox" />
<span data-i18n="Group by vendors">Group by vendors</span>
</label>
<div class="toggle-description justifyLeft wide100p">
<span data-i18n="Group by vendors Description">
Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting.
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="chutes_form" data-source="chutes">
<h4 data-i18n="Chutes API Key">Chutes API Key</h4>
@@ -3691,26 +3630,6 @@
<option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
</select>
</div>
<div class="marginTopBot5">
<div class="inline-drawer wide100p">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="Chutes Model Sorting">Chutes Model Sorting</b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content m-b-1">
<div class="marginTopBot5">
<label for="chutes_sort_models" class="checkbox_label">
<select id="chutes_sort_models">
<option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option>
<option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option>
<option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option>
<option data-i18n="Context Size" value="context_length">Context Size</option>
</select>
</label>
</div>
</div>
</div>
</div>
</div>
<div id="nanogpt_form" data-source="nanogpt">
<h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4>
@@ -4062,6 +3981,37 @@
<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>
</div>
</div>
<div id="model_sorting_form" data-source="openrouter,chutes,electronhub,nanogpt,aimlapi">
<div class="inline-drawer wide100p">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="Model Sorting">Model Sorting</b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content m-b-1">
<div class="marginTopBot5">
<label for="cc_sort_models" class="checkbox_label">
<select id="cc_sort_models">
<option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option>
<option data-i18n="Prompt Price (cheapest)" value="pricing.prompt">Prompt Price (cheapest)</option>
<option data-i18n="Completion Price (cheapest)" value="pricing.completion">Completion Price (cheapest)</option>
<option data-i18n="Context Size" value="context_length">Context Size</option>
</select>
</label>
</div>
<div class="marginTopBot5">
<label for="cc_group_models" class="checkbox_label">
<input id="cc_group_models" type="checkbox" />
<span data-i18n="Group by vendors">Group by vendors</span>
</label>
<div class="toggle-description justifyLeft wide100p">
<span data-i18n="Group by vendors Description">
Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting.
</span>
</div>
</div>
</div>
</div>
</div>
<div id="prompt_post_processing_form">
<h4>
<span data-i18n="Prompt Post-Processing">
+292 -288
View File
@@ -306,12 +306,12 @@ export const settingsToUpdate = {
min_p: ['#min_p_openai', 'min_p_openai', false, false],
repetition_penalty: ['#repetition_penalty_openai', 'repetition_penalty_openai', false, false],
max_context_unlocked: ['#oai_max_context_unlocked', 'max_context_unlocked', true, false],
group_models: ['#cc_group_models', 'group_models', true, true],
sort_models: ['#cc_sort_models', 'sort_models', false, true],
openai_model: ['#model_openai_select', 'openai_model', false, true],
claude_model: ['#model_claude_select', 'claude_model', false, true],
openrouter_model: ['#model_openrouter_select', 'openrouter_model', false, true],
openrouter_use_fallback: ['#openrouter_use_fallback', 'openrouter_use_fallback', true, true],
openrouter_group_models: ['#openrouter_group_models', 'openrouter_group_models', false, true],
openrouter_sort_models: ['#openrouter_sort_models', 'openrouter_sort_models', false, true],
openrouter_providers: ['#openrouter_providers_chat', 'openrouter_providers', false, true],
openrouter_quantizations: ['#openrouter_quantizations_chat', 'openrouter_quantizations', false, true],
openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true, true],
@@ -323,14 +323,11 @@ export const settingsToUpdate = {
perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true],
groq_model: ['#model_groq_select', 'groq_model', false, true],
chutes_model: ['#model_chutes_select', 'chutes_model', false, true],
chutes_sort_models: ['#chutes_sort_models', 'chutes_sort_models', false, true],
siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true],
siliconflow_endpoint: ['#siliconflow_endpoint', 'siliconflow_endpoint', false, true],
minimax_model: ['#model_minimax_select', 'minimax_model', false, true],
minimax_endpoint: ['#minimax_endpoint', 'minimax_endpoint', false, true],
electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true],
electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true],
electronhub_group_models: ['#electronhub_group_models', 'electronhub_group_models', false, true],
nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true],
deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true],
aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true],
@@ -428,6 +425,8 @@ const default_settings = {
group_nudge_prompt: default_group_nudge_prompt,
scenario_format: default_scenario_format,
personality_format: default_personality_format,
sort_models: 'alphabetically',
group_models: false,
openai_model: 'gpt-4-turbo',
claude_model: 'claude-sonnet-4-5',
google_model: 'gemini-2.5-pro',
@@ -438,14 +437,11 @@ const default_settings = {
perplexity_model: 'sonar-pro',
groq_model: 'llama-3.3-70b-versatile',
chutes_model: 'deepseek-ai/DeepSeek-V3-0324',
chutes_sort_models: 'alphabetically',
siliconflow_model: 'deepseek-ai/DeepSeek-V3',
siliconflow_endpoint: SILICONFLOW_ENDPOINT.GLOBAL,
minimax_model: 'MiniMax-M2.7',
minimax_endpoint: MINIMAX_ENDPOINT.GLOBAL,
electronhub_model: 'gpt-4o-mini',
electronhub_sort_models: 'alphabetically',
electronhub_group_models: false,
nanogpt_model: 'gpt-4o-mini',
deepseek_model: 'deepseek-v4-flash',
aimlapi_model: 'chatgpt-4o-latest',
@@ -469,8 +465,6 @@ const default_settings = {
custom_include_headers: '',
openrouter_model: openrouter_website_model,
openrouter_use_fallback: false,
openrouter_group_models: false,
openrouter_sort_models: 'alphabetically',
openrouter_providers: [],
openrouter_quantizations: [],
openrouter_allow_fallbacks: true,
@@ -1897,35 +1891,6 @@ function getChutesModelTemplate(option) {
`));
}
function getNanoGptModelTemplate(option) {
const model = model_list.find(x => x.id === option?.element?.value);
if (!option.id || !model) {
return option.text;
}
const inputPrice = model.pricing?.prompt;
const outputPrice = model.pricing?.completion;
let price = 'Unknown';
if (inputPrice !== undefined && outputPrice !== undefined) {
// Check if both prices are 0 (free model)
if (inputPrice === 0 && outputPrice === 0) {
price = 'Free';
} else {
price = `$${Math.round(inputPrice * 100) / 100}/$${Math.round(outputPrice * 100) / 100} in/out Mtoken`;
}
}
const contextLength = model.context_length || 'Unknown';
return $((`
<div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}">
<strong>${DOMPurify.sanitize(model.id)}</strong> | ${contextLength} ctx | <small>${price}</small>
</div>
`));
}
function calculateChutesCost() {
if (oai_settings.chat_completion_source !== chat_completion_sources.CHUTES) {
return;
@@ -1953,19 +1918,103 @@ function calculateChutesCost() {
$('#chutes_max_prompt_cost').text(cost);
}
function getNanoGptModelTemplate(option) {
const model = model_list.find(x => x.id === option?.element?.value);
if (!option.id || !model) {
return option.text;
}
const inputPrice = model.pricing?.prompt;
const outputPrice = model.pricing?.completion;
let price = 'Unknown';
if (inputPrice !== undefined && outputPrice !== undefined) {
if (inputPrice === 0 && outputPrice === 0) {
price = 'Free';
} else {
price = `$${Math.round(inputPrice * 100) / 100}/$${Math.round(outputPrice * 100) / 100} in/out Mtoken`;
}
}
const visionIcon = model.capabilities?.vision ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : '';
const reasoningIcon = model.capabilities?.reasoning ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : '';
const toolCallsIcon = model.capabilities?.tool_calling ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports tool calling"></i>' : '';
let subHtml = '';
const sub = model.subscription;
if (sub) {
if (sub.included) {
let titleText = 'Included in subscription';
let multiplierText = '';
if (sub.inputTokenMultiplier && sub.inputTokenMultiplier !== 1) {
multiplierText = ` (${sub.inputTokenMultiplier}x)`;
titleText += ` - Input Multiplier: ${sub.inputTokenMultiplier}x`;
}
subHtml = ` <small title="${titleText}"><i class="fa-solid fa-crown fa-sm"></i> Sub${multiplierText}</small>`;
} else if (sub.note) {
const safeNote = DOMPurify.sanitize(sub.note);
subHtml = ` <small title="${safeNote}"><i class="fa-solid fa-circle-info fa-sm"></i> Not in Sub</small>`;
}
}
const iconsContainer = document.createElement('span');
iconsContainer.insertAdjacentHTML('beforeend', visionIcon);
iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon);
iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon);
iconsContainer.insertAdjacentHTML('beforeend', subHtml);
const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : '';
const contextLength = model.context_length || 'Unknown';
const modelName = model.name || model.id;
return $((`
<div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}">
<strong>${DOMPurify.sanitize(modelName)}</strong> | ${contextLength} ctx | <small>${price}</small>${capabilities}
</div>
`));
}
function getAimlapiModelTemplate(option) {
const model = model_list.find(x => x.id === option?.element?.value);
if (!option.id || !model) {
return option.text;
}
const vendor = model.id.split('/')[0];
return $((`
<div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
<div><strong>${DOMPurify.sanitize(model.info?.name || model.name || model.id)}</strong> | ${vendor}</div>
</div>
`));
}
function saveModelList(data) {
model_list = data.map((model) => ({ ...model }));
model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id));
if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
model_list = openRouterSortBy(model_list, oai_settings.openrouter_sort_models);
model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.OPENROUTER);
$('#model_openrouter_select').empty();
$('#model_openrouter_select').append($('<option>', { value: openrouter_website_model, text: t`Use OpenRouter website setting` }));
if (true === oai_settings.openrouter_group_models) {
appendOpenRouterOptions(openRouterGroupByVendor(model_list), oai_settings.openrouter_group_models);
if (oai_settings.group_models) {
groupModelsByVendor(model_list, chat_completion_sources.OPENROUTER).forEach((models, vendor) => {
const optgroup = $('<optgroup>').attr('label', vendor);
models.forEach((model) => {
optgroup.append($('<option>', { value: model.id, text: model.name }));
});
$('#model_openrouter_select').append(optgroup);
});
} else {
appendOpenRouterOptions(model_list);
model_list.forEach((model) => {
$('#model_openrouter_select').append($('<option>', { value: model.id, text: model.name }));
});
}
$('#model_openrouter_select').val(oai_settings.openrouter_model).trigger('change');
@@ -2005,13 +2054,26 @@ function saveModelList(data) {
}
if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) {
model_list = model_list.filter(m => m.type === 'chat-completion');
model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.AIMLAPI);
$('#model_aimlapi_select').empty();
const chatModels = model_list.filter(m => m.type === 'chat-completion');
appendAimlapiOptions(aimlapiGroupByVendor(chatModels));
if (oai_settings.group_models) {
groupModelsByVendor(model_list, chat_completion_sources.AIMLAPI).forEach((models, vendor) => {
const optgroup = $('<optgroup>').attr('label', vendor);
models.forEach((model) => {
optgroup.append($('<option>', { value: model.id, text: model.info?.name || model.id }));
});
$('#model_aimlapi_select').append(optgroup);
});
} else {
model_list.forEach((model) => {
$('#model_aimlapi_select').append($('<option>', { value: model.id, text: model.info?.name || model.id }));
});
}
if (!oai_settings.aimlapi_model && chatModels.length > 0) {
oai_settings.aimlapi_model = chatModels[0].id;
if (!oai_settings.aimlapi_model && model_list.length > 0) {
oai_settings.aimlapi_model = model_list[0].id;
}
$('#model_aimlapi_select').val(oai_settings.aimlapi_model).trigger('change');
@@ -2034,13 +2096,22 @@ function saveModelList(data) {
if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
model_list = model_list.filter(model => model?.endpoints?.includes('/v1/chat/completions'));
model_list = electronHubSortBy(model_list, oai_settings.electronhub_sort_models);
model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.ELECTRONHUB);
$('#model_electronhub_select').empty();
const groupedList = oai_settings.electronhub_group_models ? electronHubGroupByVendor(model_list) : model_list;
appendElectronHubOptions(groupedList, oai_settings.electronhub_group_models);
if (oai_settings.group_models) {
groupModelsByVendor(model_list, chat_completion_sources.ELECTRONHUB).forEach((models, vendor) => {
const optgroup = $('<optgroup>').attr('label', vendor);
models.forEach((model) => {
optgroup.append($('<option>', { value: model.id, text: model.name }));
});
$('#model_electronhub_select').append(optgroup);
});
} else {
model_list.forEach((model) => {
$('#model_electronhub_select').append($('<option>', { value: model.id, text: model.name }));
});
}
const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) {
@@ -2052,15 +2123,21 @@ function saveModelList(data) {
if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
model_list = model_list.filter(model => typeof model.id === 'string' && !model.id.toLowerCase().includes('affine'));
model_list = chutesSortBy(model_list, oai_settings.chutes_sort_models);
model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.CHUTES);
$('#model_chutes_select').empty();
for (const model of model_list) {
const option = $('<option>').val(model.id).text(model.id);
option.attr('data-model', JSON.stringify(model));
$('#model_chutes_select').append(option);
if (oai_settings.group_models) {
groupModelsByVendor(model_list, chat_completion_sources.CHUTES).forEach((models, vendor) => {
const optgroup = $('<optgroup>').attr('label', vendor);
models.forEach((model) => {
optgroup.append($('<option>', { value: model.id, text: model.id }));
});
$('#model_chutes_select').append(optgroup);
});
} else {
model_list.forEach((model) => {
$('#model_chutes_select').append($('<option>', { value: model.id, text: model.id }));
});
}
const selectedModel = model_list.find(model => model.id === oai_settings.chutes_model);
@@ -2072,14 +2149,22 @@ function saveModelList(data) {
}
if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
model_list = sortModelsBy(model_list, oai_settings.sort_models, chat_completion_sources.NANOGPT);
$('#model_nanogpt_select').empty();
model_list.forEach((model) => {
$('#model_nanogpt_select').append(
$('<option>', {
value: model.id,
text: model.id,
}));
});
if (oai_settings.group_models) {
groupModelsByVendor(model_list, chat_completion_sources.NANOGPT).forEach((models, vendor) => {
const optgroup = $('<optgroup>').attr('label', vendor);
models.forEach((model) => {
optgroup.append($('<option>', { value: model.id, text: model.name || model.id }));
});
$('#model_nanogpt_select').append(optgroup);
});
} else {
model_list.forEach((model) => {
$('#model_nanogpt_select').append($('<option>', { value: model.id, text: model.name || model.id }));
});
}
const selectedModel = model_list.find(model => model.id === oai_settings.nanogpt_model);
if (model_list.length > 0 && (!selectedModel || !oai_settings.nanogpt_model)) {
@@ -2092,11 +2177,7 @@ function saveModelList(data) {
if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
$('#model_deepseek_select').empty();
model_list.forEach((model) => {
$('#model_deepseek_select').append(
$('<option>', {
value: model.id,
text: model.id,
}));
$('#model_deepseek_select').append($('<option>', { value: model.id, text: model.id }));
});
const selectedModel = model_list.find(model => model.id === oai_settings.deepseek_model);
@@ -2110,11 +2191,7 @@ function saveModelList(data) {
if (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) {
$('#model_pollinations_select').empty();
model_list.forEach((model) => {
$('#model_pollinations_select').append(
$('<option>', {
value: model.id,
text: model.id,
}));
$('#model_pollinations_select').append($('<option>', { value: model.id, text: model.id }));
});
const selectedModel = model_list.find(model => model.id === oai_settings.pollinations_model);
@@ -2303,181 +2380,134 @@ function saveModelList(data) {
}
}
function appendOpenRouterOptions(model_list, groupModels = false, sort = false) {
$('#model_openrouter_select').append($('<option>', { value: openrouter_website_model, text: t`Use OpenRouter website setting` }));
const appendOption = (model, parent = null) => {
(parent || $('#model_openrouter_select')).append(
$('<option>', {
value: model.id,
text: model.name,
}));
};
if (groupModels) {
model_list.forEach((models, vendor) => {
const optgroup = $(`<optgroup label="${vendor}">`);
models.forEach((model) => {
appendOption(model, optgroup);
/**
* Sorts models by the specified property for the given source.
* @param {object[]} data - Array of model objects
* @param {string} property - Sort property ('alphabetically', 'context_length', 'pricing.prompt', 'pricing.completion')
* @param {string} source - Chat Completion source (e.g., 'openrouter', 'chutes', 'electronhub', 'nanogpt')
* @returns {object[]} Sorted array of model objects
*/
function sortModelsBy(data, property, source) {
switch (source) {
case chat_completion_sources.OPENROUTER:
return data.sort((a, b) => {
if (property === 'context_length') {
return (b.context_length || 0) - (a.context_length || 0);
} else if (property === 'pricing.input' || property === 'pricing.prompt') {
return parseFloat(a.pricing?.prompt || 0) - parseFloat(b.pricing?.prompt || 0);
} else if (property === 'pricing.output' || property === 'pricing.completion') {
return parseFloat(a.pricing?.completion || 0) - parseFloat(b.pricing?.completion || 0);
} else {
return a?.name && b?.name ? a.name.localeCompare(b.name) : 0;
}
});
$('#model_openrouter_select').append(optgroup);
});
} else {
model_list.forEach((model) => {
appendOption(model);
});
}
}
const openRouterSortBy = (data, property = 'alphabetically') => {
return data.sort((a, b) => {
if (property === 'context_length') {
return b.context_length - a.context_length;
} else if (property === 'pricing.prompt') {
return parseFloat(a.pricing.prompt) - parseFloat(b.pricing.prompt);
} else {
// Alphabetically
return a?.name && b?.name && a.name.localeCompare(b.name);
}
});
};
function openRouterGroupByVendor(array) {
return array.reduce((acc, curr) => {
const vendor = curr.id.split('/')[0];
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
}
function chutesSortBy(data, property = 'alphabetically') {
return data.sort((a, b) => {
if (property === 'context_length') {
return b.context_length - a.context_length;
} else if (property === 'pricing.input') {
const aPrice = parseFloat(a.pricing?.input || 0);
const bPrice = parseFloat(b.pricing?.input || 0);
return aPrice - bPrice;
} else if (property === 'pricing.output') {
const aPrice = parseFloat(a.pricing?.output || 0);
const bPrice = parseFloat(b.pricing?.output || 0);
return aPrice - bPrice;
} else {
return a?.id && b?.id && a.id.localeCompare(b.id);
}
});
}
function appendElectronHubOptions(model_list, groupModels = false) {
const appendOption = (model, parent = null) => {
(parent || $('#model_electronhub_select')).append(
$('<option>', {
value: model.id,
text: model.name,
}));
};
if (groupModels) {
model_list.forEach((models, vendor) => {
const optgroup = $('<optgroup>').attr('label', vendor);
models.forEach((model) => {
appendOption(model, optgroup);
case chat_completion_sources.CHUTES:
return data.sort((a, b) => {
if (property === 'context_length') {
return (b.context_length || 0) - (a.context_length || 0);
} else if (property === 'pricing.input' || property === 'pricing.prompt') {
return parseFloat(a.pricing?.input || 0) - parseFloat(b.pricing?.input || 0);
} else if (property === 'pricing.output' || property === 'pricing.completion') {
return parseFloat(a.pricing?.output || 0) - parseFloat(b.pricing?.output || 0);
} else {
return a?.id && b?.id ? a.id.localeCompare(b.id) : 0;
}
});
$('#model_electronhub_select').append(optgroup);
});
} else {
model_list.forEach((model) => {
appendOption(model);
});
case chat_completion_sources.ELECTRONHUB:
return data.sort((a, b) => {
if (property === 'context_length') {
return (b.tokens || 0) - (a.tokens || 0);
} else if (property === 'pricing.input' || property === 'pricing.prompt') {
return parseFloat(a.pricing?.input || 0) - parseFloat(b.pricing?.input || 0);
} else if (property === 'pricing.output' || property === 'pricing.completion') {
return parseFloat(a.pricing?.output || 0) - parseFloat(b.pricing?.output || 0);
} else {
return a?.name && b?.name ? a.name.localeCompare(b.name) : 0;
}
});
case chat_completion_sources.NANOGPT:
return data.sort((a, b) => {
if (property === 'context_length') {
return (b.context_length || 0) - (a.context_length || 0);
} else if (property === 'pricing.input' || property === 'pricing.prompt') {
return parseFloat(a.pricing?.prompt || 0) - parseFloat(b.pricing?.prompt || 0);
} else if (property === 'pricing.output' || property === 'pricing.completion') {
return parseFloat(a.pricing?.completion || 0) - parseFloat(b.pricing?.completion || 0);
} else {
return a?.name && b?.name ? a.name.localeCompare(b.name) : 0;
}
});
case chat_completion_sources.AIMLAPI:
return data.sort((a, b) => {
if (property === 'context_length') {
return (b.info?.contextLength || 0) - (a.info?.contextLength || 0);
} else {
// No pricing information on the API. Sort alphabetically by name.
return a?.info?.name && b?.info?.name ? a.info.name.localeCompare(b.info.name) : 0;
}
});
default:
return data;
}
}
function electronHubSortBy(data, property = 'alphabetically') {
return data.sort((a, b) => {
if (property === 'context_length') {
return b.tokens - a.tokens;
} else if (property === 'pricing.input') {
return parseFloat(a.pricing.input) - parseFloat(b.pricing.input);
} else if (property === 'pricing.output') {
return parseFloat(a.pricing.output) - parseFloat(b.pricing.output);
} else {
return a?.name && b?.name && a.name.localeCompare(b.name);
}
});
}
function electronHubGroupByVendor(array) {
return array.reduce((acc, curr) => {
const vendor = String(curr?.name || curr?.id || 'Other').split(':')[0].trim() || 'Other';
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
}
function aimlapiGroupByVendor(array) {
return array.reduce((acc, curr) => {
const vendor = curr.info.developer;
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
}
function appendAimlapiOptions(model_list) {
const appendOption = (model, parent = null) => {
(parent || $('#model_aimlapi_select')).append(
$('<option>', {
value: model.id,
text: model.info?.name || model.name || model.id,
}));
};
model_list.forEach((models, vendor) => {
const optgroup = $(`<optgroup label="${vendor}">`);
models.forEach((model) => {
appendOption(model, optgroup);
});
$('#model_aimlapi_select').append(optgroup);
});
}
function getAimlapiModelTemplate(option) {
const model = model_list.find(x => x.id === option?.element?.value);
if (!option.id || !model) {
return option.text;
/**
* Groups models by vendor for the given source. If not supported, returns a map with a single entry containing all models.
* @param {object[]} array Array of model objects
* @param {string} source Chat Completion source (e.g., 'openrouter')
* @returns {Map<string, object[]>} Map of vendor to array of models
*/
function groupModelsByVendor(array, source) {
switch (source) {
case chat_completion_sources.OPENROUTER:
return array.reduce((acc, curr) => {
const vendor = curr.id.split('/')[0];
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
case chat_completion_sources.ELECTRONHUB:
return array.reduce((acc, curr) => {
const vendor = String(curr?.name || curr?.id || 'Other').split(':')[0].trim() || 'Other';
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
case chat_completion_sources.NANOGPT:
return array.reduce((acc, curr) => {
const vendorPart = /\//.test(curr.id) ? curr.id.split('/')[0] : curr.id.split('-')[0];
const vendor = String(vendorPart?.trim()?.toLowerCase() || 'Other');
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
case chat_completion_sources.CHUTES:
return array.reduce((acc, curr) => {
const vendor = curr.id.split('/')[0];
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
case chat_completion_sources.AIMLAPI:
return array.reduce((acc, curr) => {
const vendor = curr.info?.developer || 'Other';
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
default:
return new Map([['', array]]);
}
const vendor = model.id.split('/')[0];
return $((`
<div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
<div><strong>${DOMPurify.sanitize(model.info?.name || model.name || model.id)}</strong> | ${vendor}</div>
</div>
`));
}
/**
@@ -4151,6 +4181,10 @@ function migrateChatCompletionSettings(settings) {
{ oldKey: 'use_makersuite_sysprompt', oldValue: true, newKey: 'use_sysprompt', newValue: true },
{ oldKey: 'mistralai_model', oldValue: /^(mistral-medium|mistral-small)$/, newKey: 'mistralai_model', newValue: (settings.mistralai_model + '-latest') },
{ oldKey: 'deepseek_model', oldValue: /^deepseek-(chat|reasoner|coder)$/, newKey: 'deepseek_model', newValue: 'deepseek-v4-flash' },
{ oldKey: 'openrouter_sort_models', oldValue: 'alphabetically', newKey: 'sort_models', newValue: 'alphabetically' },
{ oldKey: 'openrouter_sort_models', oldValue: 'pricing.prompt', newKey: 'sort_models', newValue: 'pricing.prompt' },
{ oldKey: 'openrouter_sort_models', oldValue: 'context_length', newKey: 'sort_models', newValue: 'context_length' },
{ oldKey: 'openrouter_group_models', oldValue: true, newKey: 'group_models', newValue: true },
];
for (const migration of migrateMap) {
@@ -5845,18 +5879,6 @@ async function onModelChange() {
eventSource.emit(event_types.CHATCOMPLETION_MODEL_CHANGED, value);
}
async function onOpenrouterModelSortChange() {
await getStatusOpen();
}
async function onChutesModelSortChange() {
await getStatusOpen();
}
async function onElectronHubModelSortChange() {
await getStatusOpen();
}
async function onNewPresetClick() {
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);
@@ -6841,16 +6863,6 @@ export function initOpenAI() {
saveSettingsDebounced();
});
$('#openrouter_group_models').on('input', function () {
oai_settings.openrouter_group_models = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#openrouter_sort_models').on('input', function () {
oai_settings.openrouter_sort_models = String($(this).val());
saveSettingsDebounced();
});
$('#openrouter_allow_fallbacks').on('input', function () {
oai_settings.openrouter_allow_fallbacks = !!$(this).prop('checked');
updateOpenRouterProvidersWarning('#openrouter_providers_chat');
@@ -6862,21 +6874,6 @@ export function initOpenAI() {
saveSettingsDebounced();
});
$('#electronhub_sort_models').on('input', function () {
oai_settings.electronhub_sort_models = String($(this).val());
saveSettingsDebounced();
});
$('#chutes_sort_models').on('input', function () {
oai_settings.chutes_sort_models = String($(this).val());
saveSettingsDebounced();
});
$('#electronhub_group_models').on('input', function () {
oai_settings.electronhub_group_models = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#squash_system_messages').on('input', function () {
oai_settings.squash_system_messages = !!$(this).prop('checked');
saveSettingsDebounced();
@@ -7142,6 +7139,18 @@ export function initOpenAI() {
saveSettingsDebounced();
});
$('#cc_group_models').on('input', async () => {
oai_settings.group_models = $('#cc_group_models').prop('checked');
reconnectOpenAi();
saveSettingsDebounced();
});
$('#cc_sort_models').on('input', async () => {
oai_settings.sort_models = $('#cc_sort_models').val().toString();
reconnectOpenAi();
saveSettingsDebounced();
});
$('#api_button_openai').on('click', onConnectButtonClick);
$('#openai_reverse_proxy').on('input', onReverseProxyInput);
$('#model_openai_select').on('change', onModelChange);
@@ -7177,11 +7186,6 @@ export function initOpenAI() {
$('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount);
$('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount);
$('#model_openrouter_select').on('change', onModelChange);
$('#openrouter_group_models').on('change', onOpenrouterModelSortChange);
$('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);
$('#chutes_sort_models').on('change', onChutesModelSortChange);
$('#electronhub_group_models').on('change', onElectronHubModelSortChange);
$('#electronhub_sort_models').on('change', onElectronHubModelSortChange);
$('#model_ai21_select').on('change', onModelChange);
$('#model_mistralai_select').on('change', onModelChange);
$('#model_cohere_select').on('change', onModelChange);