feature: customizable mappings for presets per model name / chat template (#4153) * feature: customizable mappings for presets per chat template This adds the ability to assign custom context/instruct presets on a per-chat-template(-hash) basis. Users using custom templates may use this feature to auto-enable the preset of their choice, rather than the built-in defaults. * fix: do not use chat tempate based instruct preset derivation if a preset was auto-selected already This fixes issues where regex matched presets are overridden. * reset chat template hash on connect * handle derive_mappings being null * unorthodox eslint requirement adherence * button styling * un-nest derivation related properties * Migrate setting * add bind model to preset button to the miscellaneous section * switch to unified button for model preset mappings * fix bind UI * eslint * do not include koboldcpp/ggml-model-xxx.gguf in preset saving * review fixes * stray console.log / eslint * Update checkbox state if no model map exists --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -4241,6 +4241,13 @@ | ||
| 4241 | 4241 | </div> |
| 4242 | 4242 | <div> |
| 4243 | 4243 | <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4> |
| 4244 | + <div name="bindModelPresetBlock"> | |
| 4245 | + <label for="bind_model_templates" class="checkbox_label"> | |
| 4246 | + <input id="bind_model_templates" type="checkbox" /> | |
| 4247 | + <small data-i18n="Bind Model to Templates">Bind Model to Templates</small> | |
| 4248 | + <span class="fa-solid fa-circle-question" data-i18n="[title]bind_model_templates_desc" title="When connecting to an API or choosing a model, automatically activate the current Instruct and Context templates if the model name or its chat template matches the currently loaded model."></span> | |
| 4249 | + </label> | |
| 4250 | + </div> | |
| 4244 | 4251 | <div> |
| 4245 | 4252 | <small> |
| 4246 | 4253 | <span data-i18n="Non-markdown strings"> |
| @@ -1315,16 +1315,19 @@ async function getStatusTextgen() { | ||
| 1315 | 1315 | setOnlineStatus('no_connection'); |
| 1316 | 1316 | } |
| 1317 | 1317 | |
| 1318 | + power_user.chat_template_hash = ''; | |
| 1319 | + | |
| 1318 | 1320 | // Determine instruct mode preset |
| 1319 | 1321 | const autoSelected = autoSelectInstructPreset(online_status); |
| 1320 | 1322 | |
| 1321 | 1323 | const supportsTokenization = response.headers.get('x-supports-tokenization') === 'true'; |
| 1322 | 1324 | supportsTokenization ? sessionStorage.setItem(TOKENIZER_SUPPORTED_KEY, 'true') : sessionStorage.removeItem(TOKENIZER_SUPPORTED_KEY); |
| 1323 | 1325 | |
| 1324 | 1326 | const wantsInstructDerivation = !autoSelected && (power_user.instruct.enabled && power_user.instruct.derivedinstruct_derived); |
| 1325 | 1327 | const wantsContextDerivation = !autoSelected && power_user.context_derived; |
| 1326 | 1328 | const wantsContextSize = power_user.context_size_derived; |
| 1327 | 1329 | const supportsChatTemplate = [textgen_types.KOBOLDCPP, textgen_types.LLAMACPP].includes(textgen_settings.type); |
| 1330 | + | |
| 1328 | 1331 | if (supportsChatTemplate && (wantsInstructDerivation || wantsContextDerivation || wantsContextSize)) { |
| 1329 | 1332 | const response = await fetch('/api/backends/text-completions/props', { |
| 1330 | 1333 | method: 'POST', |
| @@ -1339,6 +1342,8 @@ async function getStatusTextgen() { | ||
| 1339 | 1342 | const data = await response.json(); |
| 1340 | 1343 | if (data) { |
| 1341 | 1344 | const { chat_template, chat_template_hash } = data; |
| 1345 | + power_user.chat_template_hash = chat_template_hash; | |
| 1346 | + | |
| 1342 | 1347 | if (wantsContextSize && 'default_generation_settings' in data) { |
| 1343 | 1348 | const backend_max_context = data['default_generation_settings']['n_ctx']; |
| 1344 | 1349 | const old_value = max_context; |
| @@ -1351,15 +1356,12 @@ async function getStatusTextgen() { | ||
| 1351 | 1356 | } |
| 1352 | 1357 | } |
| 1353 | 1358 | console.log(`We have chat template ${chat_template.split('\n')[0]}...`); |
| 1354 | 1359 | const templates{ context, instruct } = await deriveTemplatesFromChatTemplate(chat_template, chat_template_hash); |
| 1355 | 1360 | if (templateswantsContextDerivation && context) { |
| 1356 | 1361 | const { selectContextPreset(context, instruct{ }isAuto: =true templates}); |
| 1357 | - if (wantsContextDerivation) { | |
| 1362 | + } | |
| 1358 | - selectContextPreset(context, { isAuto: true }); | |
| 1363 | + if (wantsInstructDerivation && power_user.instruct.enabled && instruct) { | |
| 1359 | - } | |
| 1364 | + selectInstructPreset(instruct, { isAuto: true }); | |
| 1360 | - if (wantsInstructDerivation) { | |
| 1361 | - selectInstructPreset(instruct, { isAuto: true }); | |
| 1362 | - } | |
| 1363 | 1365 | } |
| 1364 | 1366 | } |
| 1365 | 1367 | } |
| @@ -94,10 +94,12 @@ const parse_derivation = derivation => (typeof derivation === 'string') ? { | ||
| 94 | 94 | 'instruct': derivation, |
| 95 | 95 | } : derivation; |
| 96 | 96 | |
| 97 | +const not_found = { context: null, instruct: null }; | |
| 98 | + | |
| 97 | 99 | export async function deriveTemplatesFromChatTemplate(chat_template, hash) { |
| 98 | 100 | if (chat_template.trim() === '') { |
| 99 | 101 | console.log('Missing chat template.'); |
| 100 | 102 | return nullnot_found; |
| 101 | 103 | } |
| 102 | 104 | |
| 103 | 105 | if (hash in hash_derivations) { |
| @@ -112,5 +114,65 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) { | ||
| 112 | 114 | } |
| 113 | 115 | |
| 114 | 116 | console.warn(`Unknown chat template hash: ${hash} for [${chat_template}]`); |
| 115 | 117 | return nullnot_found; |
| 118 | +} | |
| 119 | + | |
| 120 | +export async function bindModelTemplates(power_user, online_status) { | |
| 121 | + if (online_status === 'no_connection') { | |
| 122 | + return false; | |
| 123 | + } | |
| 124 | + | |
| 125 | + const chat_template_hash = power_user.chat_template_hash; | |
| 126 | + | |
| 127 | + const bind_model_templates = power_user.model_templates_mappings[online_status] | |
| 128 | + ?? power_user.model_templates_mappings[chat_template_hash] | |
| 129 | + ?? {}; | |
| 130 | + const bindings_match = bind_model_templates && power_user.context.preset == bind_model_templates['context'] && (!power_user.instruct.enabled || power_user.instruct.preset === bind_model_templates['instruct']); | |
| 131 | + | |
| 132 | + | |
| 133 | + const bound = []; | |
| 134 | + | |
| 135 | + if (bindings_match) { | |
| 136 | + // unmap current preset | |
| 137 | + delete power_user.model_templates_mappings[chat_template_hash]; | |
| 138 | + delete power_user.model_templates_mappings[online_status]; | |
| 139 | + toastr.info(`Context preset for ${online_status} will use defaults when loaded the next time.`); | |
| 140 | + } else { | |
| 141 | + if (power_user.context_derived) { | |
| 142 | + if (power_user.context.preset !== bind_model_templates['context']) { | |
| 143 | + bound.push(`${power_user.context.preset} context preset`); | |
| 144 | + // toastr.info(`Bound ${power_user.context.preset} preset to currently loaded model and all models that share its chat template.`); | |
| 145 | + | |
| 146 | + // map current preset to current chat template hash | |
| 147 | + bind_model_templates['context'] = power_user.context.preset; | |
| 148 | + } | |
| 149 | + } else { | |
| 150 | + toastr.warning('Note: Context derivation is disabled. Not including context preset.'); | |
| 151 | + } | |
| 152 | + if (power_user.instruct.enabled) { | |
| 153 | + if (power_user.instruct_derived) { | |
| 154 | + if (power_user.instruct.preset !== bind_model_templates['instruct']) { | |
| 155 | + bound.push(`${power_user.instruct.preset} instruct preset`); | |
| 156 | + | |
| 157 | + bind_model_templates['instruct'] = power_user.instruct.preset; | |
| 158 | + } | |
| 159 | + } else { | |
| 160 | + toastr.warning('Note: Instruct derivation is disabled. Not including instruct preset.'); | |
| 161 | + } | |
| 162 | + } | |
| 163 | + if (bound.length == 0) { | |
| 164 | + toastr.warning('No applicable presets available.'); | |
| 165 | + return false; | |
| 166 | + } | |
| 167 | + | |
| 168 | + toastr.info(`Bound ${online_status} to ${bound.join(', ')}.`); | |
| 169 | + if (!online_status.startsWith('koboldcpp/ggml-model-')) { | |
| 170 | + power_user.model_templates_mappings[online_status] = bind_model_templates; | |
| 171 | + } | |
| 172 | + if (chat_template_hash !== '') { | |
| 173 | + power_user.model_templates_mappings[chat_template_hash] = bind_model_templates; | |
| 174 | + } | |
| 175 | + } | |
| 176 | + | |
| 177 | + return true; | |
| 116 | 178 | } |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | 'use strict'; |
| 2 | 2 | |
| 3 | 3 | import { name1, name2, online_status, saveSettingsDebounced, substituteParams } from '../script.js'; |
| 4 | 4 | import { selected_group } from './group-chats.js'; |
| 5 | 5 | import { parseExampleIntoIndividual } from './openai.js'; |
| 6 | 6 | import { |
| @@ -40,7 +40,6 @@ const controls = [ | ||
| 40 | 40 | { id: 'instruct_first_input_sequence', property: 'first_input_sequence', isCheckbox: false }, |
| 41 | 41 | { id: 'instruct_last_input_sequence', property: 'last_input_sequence', isCheckbox: false }, |
| 42 | 42 | { id: 'instruct_activation_regex', property: 'activation_regex', isCheckbox: false }, |
| 43 | - { id: 'instruct_derived', property: 'derived', isCheckbox: true }, | |
| 44 | 43 | { id: 'instruct_bind_to_context', property: 'bind_to_context', isCheckbox: true }, |
| 45 | 44 | { id: 'instruct_skip_examples', property: 'skip_examples', isCheckbox: true }, |
| 46 | 45 | { id: 'instruct_names_behavior', property: 'names_behavior', isCheckbox: false }, |
| @@ -102,7 +101,7 @@ export async function loadInstructMode(data) { | ||
| 102 | 101 | |
| 103 | 102 | $('#instruct_enabled').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.enabled); |
| 104 | 103 | $('#instructSettingsBlock, #InstructSequencesColumn').toggleClass('disabled', !power_user.instruct.enabled); |
| 105 | 104 | $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.derivedinstruct_derived); |
| 106 | 105 | $('#instruct_bind_to_context').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.bind_to_context); |
| 107 | 106 | |
| 108 | 107 | controls.forEach(control => { |
| @@ -142,6 +141,19 @@ export async function loadInstructMode(data) { | ||
| 142 | 141 | } |
| 143 | 142 | |
| 144 | 143 | /** |
| 144 | + * Updates the bind model template state based on the current model, instruct and context preset. | |
| 145 | + */ | |
| 146 | +export function updateBindModelTemplatesState() { | |
| 147 | + const bind_model_templates = power_user.model_templates_mappings[online_status] ?? power_user.model_templates_mappings[power_user.chat_template_hash]; | |
| 148 | + const bindings_match = (bind_model_templates && power_user.context.preset === bind_model_templates['context'] && (!power_user.instruct.enabled || power_user.instruct.preset === bind_model_templates['instruct'])) ?? false; | |
| 149 | + const current = $('#bind_model_templates').prop('checked'); | |
| 150 | + if (bindings_match === current) { | |
| 151 | + return; // No change needed | |
| 152 | + } | |
| 153 | + $('#bind_model_templates').prop('checked', bindings_match); | |
| 154 | +} | |
| 155 | + | |
| 156 | +/** | |
| 145 | 157 | * Select context template if not already selected. |
| 146 | 158 | * @param {string} preset Preset name. |
| 147 | 159 | * @param {object} [options={}] Optional arguments. |
| @@ -161,6 +173,8 @@ export function selectContextPreset(preset, { quiet = false, isAuto = false } = | ||
| 161 | 173 | !quiet && toastr.info(`Context Template: "${preset}" ${isAuto ? 'auto-' : ''}selected`); |
| 162 | 174 | } |
| 163 | 175 | |
| 176 | + updateBindModelTemplatesState(); | |
| 177 | + | |
| 164 | 178 | saveSettingsDebounced(); |
| 165 | 179 | } |
| 166 | 180 | |
| @@ -191,6 +205,8 @@ export function selectInstructPreset(preset, { quiet = false, isAuto = false } = | ||
| 191 | 205 | !quiet && toastr.info('Instruct Mode enabled'); |
| 192 | 206 | } |
| 193 | 207 | |
| 208 | + updateBindModelTemplatesState(); | |
| 209 | + | |
| 194 | 210 | saveSettingsDebounced(); |
| 195 | 211 | } |
| 196 | 212 | |
| @@ -201,6 +217,21 @@ export function selectInstructPreset(preset, { quiet = false, isAuto = false } = | ||
| 201 | 217 | * @returns {boolean} True if instruct preset was activated by model id, false otherwise. |
| 202 | 218 | */ |
| 203 | 219 | export function autoSelectInstructPreset(modelId) { |
| 220 | + const model_templates_map = power_user.model_templates_mappings[modelId]; | |
| 221 | + | |
| 222 | + if (model_templates_map) { | |
| 223 | + const { instruct, context } = model_templates_map; | |
| 224 | + if (instruct) { | |
| 225 | + selectInstructPreset(instruct, { isAuto: true }); | |
| 226 | + } | |
| 227 | + if (context) { | |
| 228 | + selectContextPreset(context, { isAuto: true }); | |
| 229 | + } | |
| 230 | + return true; | |
| 231 | + } else { | |
| 232 | + updateBindModelTemplatesState(); | |
| 233 | + } | |
| 234 | + | |
| 204 | 235 | // If instruct mode is disabled, don't do anything |
| 205 | 236 | if (!power_user.instruct.enabled) { |
| 206 | 237 | return false; |
| @@ -747,7 +778,7 @@ jQuery(() => { | ||
| 747 | 778 | }); |
| 748 | 779 | |
| 749 | 780 | $('#instruct_derived').on('change', function () { |
| 750 | 781 | $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.derivedinstruct_derived); |
| 751 | 782 | }); |
| 752 | 783 | |
| 753 | 784 | $('#instruct_bind_to_context').on('change', function () { |
| @@ -787,6 +818,8 @@ jQuery(() => { | ||
| 787 | 818 | // Select matching context template |
| 788 | 819 | selectMatchingContextTemplate(name); |
| 789 | 820 | } |
| 821 | + | |
| 822 | + updateBindModelTemplatesState(); | |
| 790 | 823 | }); |
| 791 | 824 | |
| 792 | 825 | if (!CSS.supports('field-sizing', 'content')) { |
| @@ -25,6 +25,7 @@ import { | ||
| 25 | 25 | setActiveCharacter, |
| 26 | 26 | entitiesFilter, |
| 27 | 27 | doNewChat, |
| 28 | + online_status, | |
| 28 | 29 | messageFormatting, |
| 29 | 30 | } from '../script.js'; |
| 30 | 31 | import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js'; |
| @@ -37,6 +38,7 @@ import { | ||
| 37 | 38 | loadInstructMode, |
| 38 | 39 | names_behavior_types, |
| 39 | 40 | selectInstructPreset, |
| 41 | + updateBindModelTemplatesState, | |
| 40 | 42 | } from './instruct-mode.js'; |
| 41 | 43 | |
| 42 | 44 | import { getTagsList, tag_import_setting, tag_map, tags } from './tags.js'; |
| @@ -57,6 +59,7 @@ import { loadSystemPrompts } from './sysprompt.js'; | ||
| 57 | 59 | import { fuzzySearchCategories } from './filters.js'; |
| 58 | 60 | import { accountStorage } from './util/AccountStorage.js'; |
| 59 | 61 | import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js'; |
| 62 | +import { bindModelTemplates } from './chat-templates.js'; | |
| 60 | 63 | |
| 61 | 64 | export { |
| 62 | 65 | loadPowerUserSettings, |
| @@ -243,7 +246,6 @@ let power_user = { | ||
| 243 | 246 | macro: true, |
| 244 | 247 | names_behavior: names_behavior_types.FORCE, |
| 245 | 248 | activation_regex: '', |
| 246 | - derived: false, | |
| 247 | 249 | bind_to_context: false, |
| 248 | 250 | user_alignment_message: '', |
| 249 | 251 | system_same_as_user: false, |
| @@ -260,8 +262,12 @@ let power_user = { | ||
| 260 | 262 | names_as_stop_strings: true, |
| 261 | 263 | }, |
| 262 | 264 | |
| 265 | + chat_template_hash: '', /** the chat template hash of the currently loaded model, if any; used when deriving mappings */ | |
| 266 | + | |
| 267 | + instruct_derived: false, | |
| 263 | 268 | context_derived: false, |
| 264 | 269 | context_size_derived: false, |
| 270 | + model_templates_mappings: {}, /** user defined model identifier / chat template hash to instruct/context template mappings */ | |
| 265 | 271 | |
| 266 | 272 | sysprompt: { |
| 267 | 273 | enabled: true, |
| @@ -1567,6 +1573,13 @@ async function loadPowerUserSettings(settings, data) { | ||
| 1567 | 1573 | delete power_user.import_card_tags; |
| 1568 | 1574 | } |
| 1569 | 1575 | |
| 1576 | + if (power_user?.instruct?.derived === true) { | |
| 1577 | + power_user.instruct_derived = true; | |
| 1578 | + delete power_user.instruct.derived; | |
| 1579 | + } | |
| 1580 | + | |
| 1581 | + power_user.chat_template_hash = ''; | |
| 1582 | + | |
| 1570 | 1583 | $('#single_line').prop('checked', power_user.single_line); |
| 1571 | 1584 | $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls); |
| 1572 | 1585 | $('#world_import_dialog').prop('checked', power_user.world_import_dialog); |
| @@ -1592,6 +1605,7 @@ async function loadPowerUserSettings(settings, data) { | ||
| 1592 | 1605 | $('#encode_tags').prop('checked', power_user.encode_tags); |
| 1593 | 1606 | $('#example_messages_behavior').val(getExampleMessagesBehavior()); |
| 1594 | 1607 | $(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop('selected', true); |
| 1608 | + $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived); | |
| 1595 | 1609 | $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived); |
| 1596 | 1610 | $('#context_size_derived').prop('checked', !!power_user.context_size_derived); |
| 1597 | 1611 | |
| @@ -1910,6 +1924,7 @@ async function loadContextSettings() { | ||
| 1910 | 1924 | } |
| 1911 | 1925 | |
| 1912 | 1926 | power_user.context.preset = name; |
| 1927 | + | |
| 1913 | 1928 | contextControls.forEach(control => { |
| 1914 | 1929 | const presetValue = preset[control.property] ?? control.defaultValue; |
| 1915 | 1930 | |
| @@ -1944,6 +1959,8 @@ async function loadContextSettings() { | ||
| 1944 | 1959 | } |
| 1945 | 1960 | } |
| 1946 | 1961 | |
| 1962 | + updateBindModelTemplatesState(); | |
| 1963 | + | |
| 1947 | 1964 | saveSettingsDebounced(); |
| 1948 | 1965 | }); |
| 1949 | 1966 | } |
| @@ -3246,6 +3263,16 @@ $(document).ready(() => { | ||
| 3246 | 3263 | $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived); |
| 3247 | 3264 | }); |
| 3248 | 3265 | |
| 3266 | + $('#instruct_derived').on('input', function () { | |
| 3267 | + const value = !!$(this).prop('checked'); | |
| 3268 | + power_user.instruct_derived = value; | |
| 3269 | + saveSettingsDebounced(); | |
| 3270 | + }); | |
| 3271 | + | |
| 3272 | + $('#instruct_derived').on('change', function () { | |
| 3273 | + $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived); | |
| 3274 | + }); | |
| 3275 | + | |
| 3249 | 3276 | $('#context_size_derived').on('input', function () { |
| 3250 | 3277 | const value = !!$(this).prop('checked'); |
| 3251 | 3278 | power_user.context_size_derived = value; |
| @@ -3256,6 +3283,14 @@ $(document).ready(() => { | ||
| 3256 | 3283 | $('#context_size_derived').prop('checked', !!power_user.context_size_derived); |
| 3257 | 3284 | }); |
| 3258 | 3285 | |
| 3286 | + $('#bind_model_templates').on('input', function () { | |
| 3287 | + if (bindModelTemplates(power_user, online_status)) { | |
| 3288 | + saveSettingsDebounced(); | |
| 3289 | + } | |
| 3290 | + }); | |
| 3291 | + | |
| 3292 | + $('#bind_model_templates').on('change', updateBindModelTemplatesState); | |
| 3293 | + | |
| 3259 | 3294 | $('#always-force-name2-checkbox').change(function () { |
| 3260 | 3295 | power_user.always_force_name2 = !!$(this).prop('checked'); |
| 3261 | 3296 | saveSettingsDebounced(); |