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>

1b2976ae1d6eace7785057e00eaa2db8a22b5374

kallewoof <karljohan-alm@garage.co.jp>

Signed
5 files changed, +158 -19Ignore whitespace
public/index.html+7 -0
@@ -4241,6 +4241,13 @@
42414241 </div>
42424242 <div>
42434243 <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>
42444251 <div>
42454252 <small>
42464253 <span data-i18n="Non-markdown strings">
public/script.js+14 -12
@@ -1315,16 +1315,19 @@ async function getStatusTextgen() {
13151315 setOnlineStatus('no_connection');
13161316 }
13171317
1318+ power_user.chat_template_hash = '';
1319+
13181320 // Determine instruct mode preset
13191321 const autoSelected = autoSelectInstructPreset(online_status);
13201322
13211323 const supportsTokenization = response.headers.get('x-supports-tokenization') === 'true';
13221324 supportsTokenization ? sessionStorage.setItem(TOKENIZER_SUPPORTED_KEY, 'true') : sessionStorage.removeItem(TOKENIZER_SUPPORTED_KEY);
13231325
13241326 const wantsInstructDerivation = !autoSelected && (power_user.instruct.enabled && power_user.instruct.derivedinstruct_derived);
13251327 const wantsContextDerivation = !autoSelected && power_user.context_derived;
13261328 const wantsContextSize = power_user.context_size_derived;
13271329 const supportsChatTemplate = [textgen_types.KOBOLDCPP, textgen_types.LLAMACPP].includes(textgen_settings.type);
1330+
13281331 if (supportsChatTemplate && (wantsInstructDerivation || wantsContextDerivation || wantsContextSize)) {
13291332 const response = await fetch('/api/backends/text-completions/props', {
13301333 method: 'POST',
@@ -1339,6 +1342,8 @@ async function getStatusTextgen() {
13391342 const data = await response.json();
13401343 if (data) {
13411344 const { chat_template, chat_template_hash } = data;
1345+ power_user.chat_template_hash = chat_template_hash;
1346+
13421347 if (wantsContextSize && 'default_generation_settings' in data) {
13431348 const backend_max_context = data['default_generation_settings']['n_ctx'];
13441349 const old_value = max_context;
@@ -1351,15 +1356,12 @@ async function getStatusTextgen() {
13511356 }
13521357 }
13531358 console.log(`We have chat template ${chat_template.split('\n')[0]}...`);
13541359 const templates{ context, instruct } = await deriveTemplatesFromChatTemplate(chat_template, chat_template_hash);
13551360 if (templateswantsContextDerivation && context) {
13561361 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- }
13631365 }
13641366 }
13651367 }
public/scripts/chat-templates.js+64 -2
@@ -94,10 +94,12 @@ const parse_derivation = derivation => (typeof derivation === 'string') ? {
9494 'instruct': derivation,
9595} : derivation;
9696
97+const not_found = { context: null, instruct: null };
98+
9799export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
98100 if (chat_template.trim() === '') {
99101 console.log('Missing chat template.');
100102 return nullnot_found;
101103 }
102104
103105 if (hash in hash_derivations) {
@@ -112,5 +114,65 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
112114 }
113115
114116 console.warn(`Unknown chat template hash: ${hash} for [${chat_template}]`);
115117 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;
116178}
public/scripts/instruct-mode.js+37 -4
@@ -1,6 +1,6 @@
11'use strict';
22
33import { name1, name2, online_status, saveSettingsDebounced, substituteParams } from '../script.js';
44import { selected_group } from './group-chats.js';
55import { parseExampleIntoIndividual } from './openai.js';
66import {
@@ -40,7 +40,6 @@ const controls = [
4040 { id: 'instruct_first_input_sequence', property: 'first_input_sequence', isCheckbox: false },
4141 { id: 'instruct_last_input_sequence', property: 'last_input_sequence', isCheckbox: false },
4242 { id: 'instruct_activation_regex', property: 'activation_regex', isCheckbox: false },
43- { id: 'instruct_derived', property: 'derived', isCheckbox: true },
4443 { id: 'instruct_bind_to_context', property: 'bind_to_context', isCheckbox: true },
4544 { id: 'instruct_skip_examples', property: 'skip_examples', isCheckbox: true },
4645 { id: 'instruct_names_behavior', property: 'names_behavior', isCheckbox: false },
@@ -102,7 +101,7 @@ export async function loadInstructMode(data) {
102101
103102 $('#instruct_enabled').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.enabled);
104103 $('#instructSettingsBlock, #InstructSequencesColumn').toggleClass('disabled', !power_user.instruct.enabled);
105104 $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.derivedinstruct_derived);
106105 $('#instruct_bind_to_context').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.bind_to_context);
107106
108107 controls.forEach(control => {
@@ -142,6 +141,19 @@ export async function loadInstructMode(data) {
142141}
143142
144143/**
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+/**
145157 * Select context template if not already selected.
146158 * @param {string} preset Preset name.
147159 * @param {object} [options={}] Optional arguments.
@@ -161,6 +173,8 @@ export function selectContextPreset(preset, { quiet = false, isAuto = false } =
161173 !quiet && toastr.info(`Context Template: "${preset}" ${isAuto ? 'auto-' : ''}selected`);
162174 }
163175
176+ updateBindModelTemplatesState();
177+
164178 saveSettingsDebounced();
165179}
166180
@@ -191,6 +205,8 @@ export function selectInstructPreset(preset, { quiet = false, isAuto = false } =
191205 !quiet && toastr.info('Instruct Mode enabled');
192206 }
193207
208+ updateBindModelTemplatesState();
209+
194210 saveSettingsDebounced();
195211}
196212
@@ -201,6 +217,21 @@ export function selectInstructPreset(preset, { quiet = false, isAuto = false } =
201217 * @returns {boolean} True if instruct preset was activated by model id, false otherwise.
202218 */
203219export 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+
204235 // If instruct mode is disabled, don't do anything
205236 if (!power_user.instruct.enabled) {
206237 return false;
@@ -747,7 +778,7 @@ jQuery(() => {
747778 });
748779
749780 $('#instruct_derived').on('change', function () {
750781 $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.derivedinstruct_derived);
751782 });
752783
753784 $('#instruct_bind_to_context').on('change', function () {
@@ -787,6 +818,8 @@ jQuery(() => {
787818 // Select matching context template
788819 selectMatchingContextTemplate(name);
789820 }
821+
822+ updateBindModelTemplatesState();
790823 });
791824
792825 if (!CSS.supports('field-sizing', 'content')) {
public/scripts/power-user.js+36 -1
@@ -25,6 +25,7 @@ import {
2525 setActiveCharacter,
2626 entitiesFilter,
2727 doNewChat,
28+ online_status,
2829 messageFormatting,
2930} from '../script.js';
3031import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
@@ -37,6 +38,7 @@ import {
3738 loadInstructMode,
3839 names_behavior_types,
3940 selectInstructPreset,
41+ updateBindModelTemplatesState,
4042} from './instruct-mode.js';
4143
4244import { getTagsList, tag_import_setting, tag_map, tags } from './tags.js';
@@ -57,6 +59,7 @@ import { loadSystemPrompts } from './sysprompt.js';
5759import { fuzzySearchCategories } from './filters.js';
5860import { accountStorage } from './util/AccountStorage.js';
5961import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js';
62+import { bindModelTemplates } from './chat-templates.js';
6063
6164export {
6265 loadPowerUserSettings,
@@ -243,7 +246,6 @@ let power_user = {
243246 macro: true,
244247 names_behavior: names_behavior_types.FORCE,
245248 activation_regex: '',
246- derived: false,
247249 bind_to_context: false,
248250 user_alignment_message: '',
249251 system_same_as_user: false,
@@ -260,8 +262,12 @@ let power_user = {
260262 names_as_stop_strings: true,
261263 },
262264
265+ chat_template_hash: '', /** the chat template hash of the currently loaded model, if any; used when deriving mappings */
266+
267+ instruct_derived: false,
263268 context_derived: false,
264269 context_size_derived: false,
270+ model_templates_mappings: {}, /** user defined model identifier / chat template hash to instruct/context template mappings */
265271
266272 sysprompt: {
267273 enabled: true,
@@ -1567,6 +1573,13 @@ async function loadPowerUserSettings(settings, data) {
15671573 delete power_user.import_card_tags;
15681574 }
15691575
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+
15701583 $('#single_line').prop('checked', power_user.single_line);
15711584 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
15721585 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);
@@ -1592,6 +1605,7 @@ async function loadPowerUserSettings(settings, data) {
15921605 $('#encode_tags').prop('checked', power_user.encode_tags);
15931606 $('#example_messages_behavior').val(getExampleMessagesBehavior());
15941607 $(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop('selected', true);
1608+ $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived);
15951609 $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);
15961610 $('#context_size_derived').prop('checked', !!power_user.context_size_derived);
15971611
@@ -1910,6 +1924,7 @@ async function loadContextSettings() {
19101924 }
19111925
19121926 power_user.context.preset = name;
1927+
19131928 contextControls.forEach(control => {
19141929 const presetValue = preset[control.property] ?? control.defaultValue;
19151930
@@ -1944,6 +1959,8 @@ async function loadContextSettings() {
19441959 }
19451960 }
19461961
1962+ updateBindModelTemplatesState();
1963+
19471964 saveSettingsDebounced();
19481965 });
19491966}
@@ -3246,6 +3263,16 @@ $(document).ready(() => {
32463263 $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);
32473264 });
32483265
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+
32493276 $('#context_size_derived').on('input', function () {
32503277 const value = !!$(this).prop('checked');
32513278 power_user.context_size_derived = value;
@@ -3256,6 +3283,14 @@ $(document).ready(() => {
32563283 $('#context_size_derived').prop('checked', !!power_user.context_size_derived);
32573284 });
32583285
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+
32593294 $('#always-force-name2-checkbox').change(function () {
32603295 power_user.always_force_name2 = !!$(this).prop('checked');
32613296 saveSettingsDebounced();