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 @@
4241 </div>4241 </div>
4242 <div>4242 <div>
4243 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>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 <div>4251 <div>
4245 <small>4252 <small>
4246 <span data-i18n="Non-markdown strings">4253 <span data-i18n="Non-markdown strings">
public/script.js+14 -12
@@ -1315,16 +1315,19 @@ async function getStatusTextgen() {
1315 setOnlineStatus('no_connection');1315 setOnlineStatus('no_connection');
1316 }1316 }
13171317
1318 power_user.chat_template_hash = '';
1319
1318 // Determine instruct mode preset1320 // Determine instruct mode preset
1319 autoSelectInstructPreset(online_status);1321 const autoSelected = autoSelectInstructPreset(online_status);
13201322
1321 const supportsTokenization = response.headers.get('x-supports-tokenization') === 'true';1323 const supportsTokenization = response.headers.get('x-supports-tokenization') === 'true';
1322 supportsTokenization ? sessionStorage.setItem(TOKENIZER_SUPPORTED_KEY, 'true') : sessionStorage.removeItem(TOKENIZER_SUPPORTED_KEY);1324 supportsTokenization ? sessionStorage.setItem(TOKENIZER_SUPPORTED_KEY, 'true') : sessionStorage.removeItem(TOKENIZER_SUPPORTED_KEY);
13231325
1324 const wantsInstructDerivation = (power_user.instruct.enabled && power_user.instruct.derived);1326 const wantsInstructDerivation = !autoSelected && (power_user.instruct.enabled && power_user.instruct_derived);
1325 const wantsContextDerivation = power_user.context_derived;1327 const wantsContextDerivation = !autoSelected && power_user.context_derived;
1326 const wantsContextSize = power_user.context_size_derived;1328 const wantsContextSize = power_user.context_size_derived;
1327 const supportsChatTemplate = [textgen_types.KOBOLDCPP, textgen_types.LLAMACPP].includes(textgen_settings.type);1329 const supportsChatTemplate = [textgen_types.KOBOLDCPP, textgen_types.LLAMACPP].includes(textgen_settings.type);
1330
1328 if (supportsChatTemplate && (wantsInstructDerivation || wantsContextDerivation || wantsContextSize)) {1331 if (supportsChatTemplate && (wantsInstructDerivation || wantsContextDerivation || wantsContextSize)) {
1329 const response = await fetch('/api/backends/text-completions/props', {1332 const response = await fetch('/api/backends/text-completions/props', {
1330 method: 'POST',1333 method: 'POST',
@@ -1339,6 +1342,8 @@ async function getStatusTextgen() {
1339 const data = await response.json();1342 const data = await response.json();
1340 if (data) {1343 if (data) {
1341 const { chat_template, chat_template_hash } = data;1344 const { chat_template, chat_template_hash } = data;
1345 power_user.chat_template_hash = chat_template_hash;
1346
1342 if (wantsContextSize && 'default_generation_settings' in data) {1347 if (wantsContextSize && 'default_generation_settings' in data) {
1343 const backend_max_context = data['default_generation_settings']['n_ctx'];1348 const backend_max_context = data['default_generation_settings']['n_ctx'];
1344 const old_value = max_context;1349 const old_value = max_context;
@@ -1351,15 +1356,12 @@ async function getStatusTextgen() {
1351 }1356 }
1352 }1357 }
1353 console.log(`We have chat template ${chat_template.split('\n')[0]}...`);1358 console.log(`We have chat template ${chat_template.split('\n')[0]}...`);
1354 const templates = await deriveTemplatesFromChatTemplate(chat_template, chat_template_hash);1359 const { context, instruct } = await deriveTemplatesFromChatTemplate(chat_template, chat_template_hash);
1355 if (templates) {1360 if (wantsContextDerivation && context) {
1356 const { context, instruct } = templates;1361 selectContextPreset(context, { isAuto: true });
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 }
public/scripts/chat-templates.js+64 -2
@@ -94,10 +94,12 @@ const parse_derivation = derivation => (typeof derivation === 'string') ? {
94 'instruct': derivation,94 'instruct': derivation,
95} : derivation;95} : derivation;
9696
97const not_found = { context: null, instruct: null };
98
97export async function deriveTemplatesFromChatTemplate(chat_template, hash) {99export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
98 if (chat_template.trim() === '') {100 if (chat_template.trim() === '') {
99 console.log('Missing chat template.');101 console.log('Missing chat template.');
100 return null;102 return not_found;
101 }103 }
102104
103 if (hash in hash_derivations) {105 if (hash in hash_derivations) {
@@ -112,5 +114,65 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
112 }114 }
113115
114 console.warn(`Unknown chat template hash: ${hash} for [${chat_template}]`);116 console.warn(`Unknown chat template hash: ${hash} for [${chat_template}]`);
115 return null;117 return not_found;
118}
119
120export 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}
public/scripts/instruct-mode.js+37 -4
@@ -1,6 +1,6 @@
1'use strict';1'use strict';
22
3import { name1, name2, saveSettingsDebounced, substituteParams } from '../script.js';3import { name1, name2, online_status, saveSettingsDebounced, substituteParams } from '../script.js';
4import { selected_group } from './group-chats.js';4import { selected_group } from './group-chats.js';
5import { parseExampleIntoIndividual } from './openai.js';5import { parseExampleIntoIndividual } from './openai.js';
6import {6import {
@@ -40,7 +40,6 @@ const controls = [
40 { id: 'instruct_first_input_sequence', property: 'first_input_sequence', isCheckbox: false },40 { id: 'instruct_first_input_sequence', property: 'first_input_sequence', isCheckbox: false },
41 { id: 'instruct_last_input_sequence', property: 'last_input_sequence', isCheckbox: false },41 { id: 'instruct_last_input_sequence', property: 'last_input_sequence', isCheckbox: false },
42 { id: 'instruct_activation_regex', property: 'activation_regex', isCheckbox: false },42 { id: 'instruct_activation_regex', property: 'activation_regex', isCheckbox: false },
43 { id: 'instruct_derived', property: 'derived', isCheckbox: true },
44 { id: 'instruct_bind_to_context', property: 'bind_to_context', isCheckbox: true },43 { id: 'instruct_bind_to_context', property: 'bind_to_context', isCheckbox: true },
45 { id: 'instruct_skip_examples', property: 'skip_examples', isCheckbox: true },44 { id: 'instruct_skip_examples', property: 'skip_examples', isCheckbox: true },
46 { id: 'instruct_names_behavior', property: 'names_behavior', isCheckbox: false },45 { id: 'instruct_names_behavior', property: 'names_behavior', isCheckbox: false },
@@ -102,7 +101,7 @@ export async function loadInstructMode(data) {
102101
103 $('#instruct_enabled').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.enabled);102 $('#instruct_enabled').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.enabled);
104 $('#instructSettingsBlock, #InstructSequencesColumn').toggleClass('disabled', !power_user.instruct.enabled);103 $('#instructSettingsBlock, #InstructSequencesColumn').toggleClass('disabled', !power_user.instruct.enabled);
105 $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.derived);104 $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived);
106 $('#instruct_bind_to_context').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.bind_to_context);105 $('#instruct_bind_to_context').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.bind_to_context);
107106
108 controls.forEach(control => {107 controls.forEach(control => {
@@ -142,6 +141,19 @@ export async function loadInstructMode(data) {
142}141}
143142
144/**143/**
144 * Updates the bind model template state based on the current model, instruct and context preset.
145 */
146export 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 * Select context template if not already selected.157 * Select context template if not already selected.
146 * @param {string} preset Preset name.158 * @param {string} preset Preset name.
147 * @param {object} [options={}] Optional arguments.159 * @param {object} [options={}] Optional arguments.
@@ -161,6 +173,8 @@ export function selectContextPreset(preset, { quiet = false, isAuto = false } =
161 !quiet && toastr.info(`Context Template: "${preset}" ${isAuto ? 'auto-' : ''}selected`);173 !quiet && toastr.info(`Context Template: "${preset}" ${isAuto ? 'auto-' : ''}selected`);
162 }174 }
163175
176 updateBindModelTemplatesState();
177
164 saveSettingsDebounced();178 saveSettingsDebounced();
165}179}
166180
@@ -191,6 +205,8 @@ export function selectInstructPreset(preset, { quiet = false, isAuto = false } =
191 !quiet && toastr.info('Instruct Mode enabled');205 !quiet && toastr.info('Instruct Mode enabled');
192 }206 }
193207
208 updateBindModelTemplatesState();
209
194 saveSettingsDebounced();210 saveSettingsDebounced();
195}211}
196212
@@ -201,6 +217,21 @@ export function selectInstructPreset(preset, { quiet = false, isAuto = false } =
201 * @returns {boolean} True if instruct preset was activated by model id, false otherwise.217 * @returns {boolean} True if instruct preset was activated by model id, false otherwise.
202 */218 */
203export function autoSelectInstructPreset(modelId) {219export 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 // If instruct mode is disabled, don't do anything235 // If instruct mode is disabled, don't do anything
205 if (!power_user.instruct.enabled) {236 if (!power_user.instruct.enabled) {
206 return false;237 return false;
@@ -747,7 +778,7 @@ jQuery(() => {
747 });778 });
748779
749 $('#instruct_derived').on('change', function () {780 $('#instruct_derived').on('change', function () {
750 $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.derived);781 $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived);
751 });782 });
752783
753 $('#instruct_bind_to_context').on('change', function () {784 $('#instruct_bind_to_context').on('change', function () {
@@ -787,6 +818,8 @@ jQuery(() => {
787 // Select matching context template818 // Select matching context template
788 selectMatchingContextTemplate(name);819 selectMatchingContextTemplate(name);
789 }820 }
821
822 updateBindModelTemplatesState();
790 });823 });
791824
792 if (!CSS.supports('field-sizing', 'content')) {825 if (!CSS.supports('field-sizing', 'content')) {
public/scripts/power-user.js+36 -1
@@ -25,6 +25,7 @@ import {
25 setActiveCharacter,25 setActiveCharacter,
26 entitiesFilter,26 entitiesFilter,
27 doNewChat,27 doNewChat,
28 online_status,
28 messageFormatting,29 messageFormatting,
29} from '../script.js';30} from '../script.js';
30import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';31import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
@@ -37,6 +38,7 @@ import {
37 loadInstructMode,38 loadInstructMode,
38 names_behavior_types,39 names_behavior_types,
39 selectInstructPreset,40 selectInstructPreset,
41 updateBindModelTemplatesState,
40} from './instruct-mode.js';42} from './instruct-mode.js';
4143
42import { getTagsList, tag_import_setting, tag_map, tags } from './tags.js';44import { getTagsList, tag_import_setting, tag_map, tags } from './tags.js';
@@ -57,6 +59,7 @@ import { loadSystemPrompts } from './sysprompt.js';
57import { fuzzySearchCategories } from './filters.js';59import { fuzzySearchCategories } from './filters.js';
58import { accountStorage } from './util/AccountStorage.js';60import { accountStorage } from './util/AccountStorage.js';
59import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js';61import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js';
62import { bindModelTemplates } from './chat-templates.js';
6063
61export {64export {
62 loadPowerUserSettings,65 loadPowerUserSettings,
@@ -243,7 +246,6 @@ let power_user = {
243 macro: true,246 macro: true,
244 names_behavior: names_behavior_types.FORCE,247 names_behavior: names_behavior_types.FORCE,
245 activation_regex: '',248 activation_regex: '',
246 derived: false,
247 bind_to_context: false,249 bind_to_context: false,
248 user_alignment_message: '',250 user_alignment_message: '',
249 system_same_as_user: false,251 system_same_as_user: false,
@@ -260,8 +262,12 @@ let power_user = {
260 names_as_stop_strings: true,262 names_as_stop_strings: true,
261 },263 },
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,
263 context_derived: false,268 context_derived: false,
264 context_size_derived: false,269 context_size_derived: false,
270 model_templates_mappings: {}, /** user defined model identifier / chat template hash to instruct/context template mappings */
265271
266 sysprompt: {272 sysprompt: {
267 enabled: true,273 enabled: true,
@@ -1567,6 +1573,13 @@ async function loadPowerUserSettings(settings, data) {
1567 delete power_user.import_card_tags;1573 delete power_user.import_card_tags;
1568 }1574 }
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
1570 $('#single_line').prop('checked', power_user.single_line);1583 $('#single_line').prop('checked', power_user.single_line);
1571 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);1584 $('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
1572 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);1585 $('#world_import_dialog').prop('checked', power_user.world_import_dialog);
@@ -1592,6 +1605,7 @@ async function loadPowerUserSettings(settings, data) {
1592 $('#encode_tags').prop('checked', power_user.encode_tags);1605 $('#encode_tags').prop('checked', power_user.encode_tags);
1593 $('#example_messages_behavior').val(getExampleMessagesBehavior());1606 $('#example_messages_behavior').val(getExampleMessagesBehavior());
1594 $(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop('selected', true);1607 $(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop('selected', true);
1608 $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived);
1595 $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);1609 $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);
1596 $('#context_size_derived').prop('checked', !!power_user.context_size_derived);1610 $('#context_size_derived').prop('checked', !!power_user.context_size_derived);
15971611
@@ -1910,6 +1924,7 @@ async function loadContextSettings() {
1910 }1924 }
19111925
1912 power_user.context.preset = name;1926 power_user.context.preset = name;
1927
1913 contextControls.forEach(control => {1928 contextControls.forEach(control => {
1914 const presetValue = preset[control.property] ?? control.defaultValue;1929 const presetValue = preset[control.property] ?? control.defaultValue;
19151930
@@ -1944,6 +1959,8 @@ async function loadContextSettings() {
1944 }1959 }
1945 }1960 }
19461961
1962 updateBindModelTemplatesState();
1963
1947 saveSettingsDebounced();1964 saveSettingsDebounced();
1948 });1965 });
1949}1966}
@@ -3246,6 +3263,16 @@ $(document).ready(() => {
3246 $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);3263 $('#context_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.context_derived);
3247 });3264 });
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
3249 $('#context_size_derived').on('input', function () {3276 $('#context_size_derived').on('input', function () {
3250 const value = !!$(this).prop('checked');3277 const value = !!$(this).prop('checked');
3251 power_user.context_size_derived = value;3278 power_user.context_size_derived = value;
@@ -3256,6 +3283,14 @@ $(document).ready(() => {
3256 $('#context_size_derived').prop('checked', !!power_user.context_size_derived);3283 $('#context_size_derived').prop('checked', !!power_user.context_size_derived);
3257 });3284 });
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
3259 $('#always-force-name2-checkbox').change(function () {3294 $('#always-force-name2-checkbox').change(function () {
3260 power_user.always_force_name2 = !!$(this).prop('checked');3295 power_user.always_force_name2 = !!$(this).prop('checked');
3261 saveSettingsDebounced();3296 saveSettingsDebounced();