diff --git a/public/index.html b/public/index.html index 9f541c7ae..0882f472e 100644 --- a/public/index.html +++ b/public/index.html @@ -3224,36 +3224,6 @@ Allow fallback models -
-
-
- OpenRouter Model Sorting -
-
-
-
- -
-
- -
- - Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting. - -
-
-
-
-

Model Providers @@ -3641,37 +3611,6 @@

-
-
-
- Electron Hub Model Sorting -
-
-
-
- -
-
- -
- - Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting. - -
-
-
-
-

Chutes API Key

@@ -3691,26 +3630,6 @@
-
-
-
- Chutes Model Sorting -
-
-
-
- -
-
-
-

NanoGPT API Key

@@ -4062,6 +3981,37 @@ The underlying model of your deployment. This is detected automatically when you connect.
+
+
+
+ Model Sorting +
+
+
+
+ +
+
+ +
+ + Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting. + +
+
+
+
+

diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 66a434c6c..6ab09cf67 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -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 $((` -
- ${DOMPurify.sanitize(model.id)} | ${contextLength} ctx | ${price} -
- `)); -} - 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 ? '' : ''; + const reasoningIcon = model.capabilities?.reasoning ? '' : ''; + const toolCallsIcon = model.capabilities?.tool_calling ? '' : ''; + + 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 = ` Sub${multiplierText}`; + } else if (sub.note) { + const safeNote = DOMPurify.sanitize(sub.note); + subHtml = ` Not in Sub`; + } + } + + 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 $((` +
+ ${DOMPurify.sanitize(modelName)} | ${contextLength} ctx | ${price}${capabilities} +
+ `)); +} + +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 $((` +
+
${DOMPurify.sanitize(model.info?.name || model.name || model.id)} | ${vendor}
+
+ `)); +} + 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($('').attr('label', vendor); + models.forEach((model) => { + optgroup.append($('').attr('label', vendor); + models.forEach((model) => { + optgroup.append($('').attr('label', vendor); + models.forEach((model) => { + optgroup.append($('').attr('label', vendor); + models.forEach((model) => { + optgroup.append($('').attr('label', vendor); + models.forEach((model) => { + optgroup.append($('`); - - 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( - $('').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( - $('`); - - 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} 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 $((` -
-
${DOMPurify.sanitize(model.info?.name || model.name || model.id)} | ${vendor}
-
- `)); } /** @@ -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);