Merge pull request #3700 from SillyTavern/reasoning-template Reasoning templates

03d590415b9f600082965b1f52ab39bacfa3f5b4

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
13 files changed, +273 -22Ignore whitespace
default/content/index.json+8 -0
@@ -786,5 +786,13 @@
786786 {
787787 "filename": "presets/context/DeepSeek-V2.5.json",
788788 "type": "context"
789+ },
790+ {
791+ "filename": "presets/reasoning/DeepSeek.json",
792+ "type": "reasoning"
793+ },
794+ {
795+ "filename": "presets/reasoning/Blank.json",
796+ "type": "reasoning"
789797 }
790798]
default/content/presets/reasoning/Blank.json+6 -0
@@ -0,0 +1,6 @@
1+{
2+ "name": "Blank",
3+ "prefix": "",
4+ "suffix": "",
5+ "separator": ""
6+}
default/content/presets/reasoning/DeepSeek.json+6 -0
@@ -0,0 +1,6 @@
1+{
2+ "name": "DeepSeek",
3+ "prefix": "<think>\n",
4+ "suffix": "\n</think>",
5+ "separator": "\n\n"
6+}
public/index.html+13 -0
@@ -3917,6 +3917,19 @@
39173917 <summary data-i18n="Reasoning Formatting">
39183918 Reasoning Formatting
39193919 </summary>
3920+ <div class="flex-container" title="Select your current Reasoning Template" data-i18n="[title]Select your current Reasoning Template">
3921+ <select id="reasoning_select" data-preset-manager-for="reasoning" class="flex1 text_pole"></select>
3922+ <div class="flex-container margin0 justifyCenter gap3px">
3923+ <input type="file" hidden data-preset-manager-file="reasoning" accept=".json, .settings">
3924+ <i data-preset-manager-update="reasoning" class="menu_button fa-solid fa-save" title="Update current template" data-i18n="[title]Update current template"></i>
3925+ <i data-preset-manager-rename="reasoning" class="menu_button fa-pencil fa-solid" title="Rename current template" data-i18n="[title]Rename current template"></i>
3926+ <i data-preset-manager-new="reasoning" class="menu_button fa-solid fa-file-circle-plus" title="Save template as" data-i18n="[title]Save template as"></i>
3927+ <i data-preset-manager-import="reasoning" class="displayNone menu_button fa-solid fa-file-import" title="Import template" data-i18n="[title]Import template"></i>
3928+ <i data-preset-manager-export="reasoning" class="displayNone menu_button fa-solid fa-file-export" title="Export template" data-i18n="[title]Export template"></i>
3929+ <i data-preset-manager-restore="reasoning" class="menu_button fa-solid fa-recycle" title="Restore current template" data-i18n="[title]Restore current template"></i>
3930+ <i data-preset-manager-delete="reasoning" class="menu_button fa-solid fa-trash-can" title="Delete template" data-i18n="[title]Delete template"></i>
3931+ </div>
3932+ </div>
39203933 <div class="flex-container">
39213934 <div class="flex1" title="Inserted before the reasoning content." data-i18n="[title]reasoning_prefix">
39223935 <small data-i18n="Prefix">Prefix</small>
public/scripts/extensions/connection-manager/index.js+4 -0
@@ -39,6 +39,7 @@ const CC_COMMANDS = [
3939 'proxy',
4040 'stop-strings',
4141 'start-reply-with',
42+ 'reasoning-template',
4243];
4344
4445const TC_COMMANDS = [
@@ -54,6 +55,7 @@ const TC_COMMANDS = [
5455 'tokenizer',
5556 'stop-strings',
5657 'start-reply-with',
58+ 'reasoning-template',
5759];
5860
5961const FANCY_NAMES = {
@@ -70,6 +72,7 @@ const FANCY_NAMES = {
7072 'tokenizer': 'Tokenizer',
7173 'stop-strings': 'Custom Stopping Strings',
7274 'start-reply-with': 'Start Reply With',
75+ 'reasoning-template': 'Reasoning Template',
7376};
7477
7578/**
@@ -154,6 +157,7 @@ const profilesProvider = () => [
154157 * @property {string} [tokenizer] Tokenizer
155158 * @property {string} [stop-strings] Custom Stopping Strings
156159 * @property {string} [start-reply-with] Start Reply With
160+ * @property {string} [reasoning-template] Reasoning Template
157161 * @property {string[]} [exclude] Commands to exclude
158162 */
159163
public/scripts/power-user.js+3 -0
@@ -55,6 +55,7 @@ import { POPUP_TYPE, callGenericPopup } from './popup.js';
5555import { loadSystemPrompts } from './sysprompt.js';
5656import { fuzzySearchCategories } from './filters.js';
5757import { accountStorage } from './util/AccountStorage.js';
58+import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js';
5859
5960export {
6061 loadPowerUserSettings,
@@ -257,6 +258,7 @@ let power_user = {
257258 },
258259
259260 reasoning: {
261+ name: DEFAULT_REASONING_TEMPLATE,
260262 auto_parse: false,
261263 add_to_prompts: false,
262264 auto_expand: false,
@@ -1624,6 +1626,7 @@ async function loadPowerUserSettings(settings, data) {
16241626 await loadInstructMode(data);
16251627 await loadContextSettings();
16261628 await loadSystemPrompts(data);
1629+ await loadReasoningTemplates(data);
16271630 loadMaxContextUnlocked();
16281631 switchWaifuMode();
16291632 switchSpoilerMode();
public/scripts/preset-manager.js+44 -2
@@ -21,7 +21,7 @@ import { groups, selected_group } from './group-chats.js';
2121import { instruct_presets } from './instruct-mode.js';
2222import { kai_settings } from './kai-settings.js';
2323import { convertNovelPreset } from './nai-settings.js';
2424import { openai_settings, openai_setting_names, oai_settings } from './openai.js';
2525import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
2626import { context_presets, getContextSettings, power_user } from './power-user.js';
2727import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -38,6 +38,7 @@ import {
3838} from './textgen-settings.js';
3939import { download, parseJsonFile, waitUntilCondition } from './utils.js';
4040import { t } from './i18n.js';
41+import { reasoning_templates } from './reasoning.js';
4142
4243const presetManagers = {};
4344
@@ -168,6 +169,20 @@ class PresetManager {
168169 },
169170 isValid: (data) => PresetManager.isPossiblyTextCompletionData(data),
170171 },
172+ 'reasoning': {
173+ name: 'Reasoning Formatting',
174+ getData: () => {
175+ const manager = getPresetManager('reasoning');
176+ const name = manager.getSelectedPresetName();
177+ return manager.getPresetSettings(name);
178+ },
179+ setData: (data) => {
180+ const manager = getPresetManager('reasoning');
181+ const name = data.name;
182+ return manager.savePreset(name, data);
183+ },
184+ isValid: (data) => PresetManager.isPossiblyReasoningData(data),
185+ },
171186 };
172187
173188 static isPossiblyInstructData(data) {
@@ -190,6 +205,11 @@ class PresetManager {
190205 return data && textCompletionProps.every(prop => Object.keys(data).includes(prop));
191206 }
192207
208+ static isPossiblyReasoningData(data) {
209+ const reasoningProps = ['name', 'prefix', 'suffix', 'separator'];
210+ return data && reasoningProps.every(prop => Object.keys(data).includes(prop));
211+ }
212+
193213 /**
194214 * Imports master settings from JSON data.
195215 * @param {object} data Data to import
@@ -227,6 +247,12 @@ class PresetManager {
227247 return await getPresetManager('textgenerationwebui').savePreset(fileName, data);
228248 }
229249
250+ // 5. Reasoning Template
251+ if (this.isPossiblyReasoningData(data)) {
252+ toastr.info(t`Importing as reasoning template...`, t`Reasoning template detected`);
253+ return await getPresetManager('reasoning').savePreset(data.name, data);
254+ }
255+
230256 const validSections = [];
231257 for (const [key, section] of Object.entries(this.masterSections)) {
232258 if (key in data && section.isValid(data[key])) {
@@ -478,6 +504,10 @@ class PresetManager {
478504 presets = system_prompts;
479505 preset_names = system_prompts.map(x => x.name);
480506 break;
507+ case 'reasoning':
508+ presets = reasoning_templates;
509+ preset_names = reasoning_templates.map(x => x.name);
510+ break;
481511 default:
482512 console.warn(`Unknown API ID ${api}`);
483513 }
@@ -490,7 +520,7 @@ class PresetManager {
490520 }
491521
492522 isAdvancedFormatting() {
493- return this.apiId == 'context' || this.apiId == 'instruct' || this.apiId == 'sysprompt';
523+ return ['context', 'instruct', 'sysprompt', 'reasoning'].includes(this.apiId);
494524 }
495525
496526 updateList(name, preset) {
@@ -553,6 +583,11 @@ class PresetManager {
553583 sysprompt_preset['name'] = name || power_user.sysprompt.preset;
554584 return sysprompt_preset;
555585 }
586+ case 'reasoning': {
587+ const reasoning_preset = structuredClone(power_user.reasoning);
588+ reasoning_preset['name'] = name || power_user.reasoning.preset;
589+ return reasoning_preset;
590+ }
556591 default:
557592 console.warn(`Unknown API ID ${apiId}`);
558593 return {};
@@ -599,6 +634,13 @@ class PresetManager {
599634 'include_reasoning',
600635 'global_banned_tokens',
601636 'send_banned_tokens',
637+
638+ // Reasoning exclusions
639+ 'auto_parse',
640+ 'add_to_prompts',
641+ 'auto_expand',
642+ 'show_hidden',
643+ 'max_additions',
602644 ];
603645 const settings = Object.assign({}, getSettingsByApiId(this.apiId));
604646
public/scripts/reasoning.js+179 -19
@@ -7,14 +7,46 @@ import { getCurrentLocale, t, translate } from './i18n.js';
77import { MacrosParser } from './macros.js';
88import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
99import { Popup } from './popup.js';
1010import { performFuzzySearch, power_user } from './power-user.js';
11+import { getPresetManager } from './preset-manager.js';
1112import { SlashCommand } from './slash-commands/SlashCommand.js';
1213import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
1314import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
1415import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
1516import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
1617import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
1718import { copyText, escapeRegex, isFalseBoolean, isTrueBoolean, setDatasetProperty, trimSpaces } from './utils.js';
19+
20+/**
21+ * @typedef {object} ReasoningTemplate
22+ * @property {string} name - The name of the template
23+ * @property {string} prefix - Reasoning prefix
24+ * @property {string} suffix - Reasoning suffix
25+ * @property {string} separator - Reasoning separator
26+ */
27+
28+/**
29+ * @type {ReasoningTemplate[]} List of reasoning templates
30+ */
31+export const reasoning_templates = [];
32+
33+export const DEFAULT_REASONING_TEMPLATE = 'DeepSeek';
34+
35+/**
36+ * @type {Record<string, JQuery<HTMLElement>>} List of UI elements for reasoning settings
37+ * @readonly
38+ */
39+const UI = {
40+ $select: $('#reasoning_select'),
41+ $suffix: $('#reasoning_suffix'),
42+ $prefix: $('#reasoning_prefix'),
43+ $separator: $('#reasoning_separator'),
44+ $autoParse: $('#reasoning_auto_parse'),
45+ $autoExpand: $('#reasoning_auto_expand'),
46+ $showHidden: $('#reasoning_show_hidden'),
47+ $addToPrompts: $('#reasoning_add_to_prompts'),
48+ $maxAdditions: $('#reasoning_max_additions'),
49+};
1850
1951/**
2052 * Enum representing the type of the reasoning for a message (where it came from)
@@ -61,7 +93,7 @@ export function extractReasoningFromData(data, {
6193 mainApi = null,
6294 ignoreShowThoughts = false,
6395 textGenType = null,
6496 chatCompletionSource = null,
6597} = {}) {
6698 switch (mainApi ?? main_api) {
6799 case 'textgenerationwebui':
@@ -669,57 +701,102 @@ export class PromptReasoning {
669701}
670702
671703function loadReasoningSettings() {
672704 UI.$('#reasoning_add_to_prompts')addToPrompts.prop('checked', power_user.reasoning.add_to_prompts);
673705 UI.$('#reasoning_add_to_prompts')addToPrompts.on('change', function () {
674706 power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
675707 saveSettingsDebounced();
676708 });
677709
678710 UI.$('#reasoning_prefix')prefix.val(power_user.reasoning.prefix);
679711 UI.$('#reasoning_prefix')prefix.on('input', function () {
680712 power_user.reasoning.prefix = String($(this).val());
681713 saveSettingsDebounced();
682714 });
683715
684716 UI.$('#reasoning_suffix')suffix.val(power_user.reasoning.suffix);
685717 UI.$('#reasoning_suffix')suffix.on('input', function () {
686718 power_user.reasoning.suffix = String($(this).val());
687719 saveSettingsDebounced();
688720 });
689721
690722 UI.$('#reasoning_separator')separator.val(power_user.reasoning.separator);
691723 UI.$('#reasoning_separator')separator.on('input', function () {
692724 power_user.reasoning.separator = String($(this).val());
693725 saveSettingsDebounced();
694726 });
695727
696728 UI.$('#reasoning_max_additions')maxAdditions.val(power_user.reasoning.max_additions);
697729 UI.$('#reasoning_max_additions')maxAdditions.on('input', function () {
698730 power_user.reasoning.max_additions = Number($(this).val());
699731 saveSettingsDebounced();
700732 });
701733
702734 UI.$('#reasoning_auto_parse')autoParse.prop('checked', power_user.reasoning.auto_parse);
703735 UI.$('#reasoning_auto_parse')autoParse.on('change', function () {
704736 power_user.reasoning.auto_parse = !!$(this).prop('checked');
705737 saveSettingsDebounced();
706738 });
707739
708740 UI.$('#reasoning_auto_expand')autoExpand.prop('checked', power_user.reasoning.auto_expand);
709741 UI.$('#reasoning_auto_expand')autoExpand.on('change', function () {
710742 power_user.reasoning.auto_expand = !!$(this).prop('checked');
711743 toggleReasoningAutoExpand();
712744 saveSettingsDebounced();
713745 });
714746 toggleReasoningAutoExpand();
715747
716748 UI.$('#reasoning_show_hidden')showHidden.prop('checked', power_user.reasoning.show_hidden);
717749 UI.$('#reasoning_show_hidden')showHidden.on('change', function () {
718750 power_user.reasoning.show_hidden = !!$(this).prop('checked');
719751 $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
720752 saveSettingsDebounced();
721753 });
722754 $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
755+
756+ UI.$select.on('change', async function () {
757+ const name = String($(this).val());
758+ const template = reasoning_templates.find(p => p.name === name);
759+ if (!template) {
760+ return;
761+ }
762+
763+ UI.$prefix.val(template.prefix);
764+ UI.$suffix.val(template.suffix);
765+ UI.$separator.val(template.separator);
766+
767+ power_user.reasoning.name = name;
768+ power_user.reasoning.prefix = template.prefix;
769+ power_user.reasoning.suffix = template.suffix;
770+ power_user.reasoning.separator = template.separator;
771+
772+ saveSettingsDebounced();
773+ });
774+}
775+
776+function selectReasoningTemplateCallback(args, name) {
777+ if (!name) {
778+ return power_user.reasoning.name ?? '';
779+ }
780+
781+ const quiet = isTrueBoolean(args?.quiet);
782+ const templateNames = reasoning_templates.map(preset => preset.name);
783+ let foundName = templateNames.find(x => x.toLowerCase() === name.toLowerCase());
784+
785+ if (!foundName) {
786+ const result = performFuzzySearch('reasoning-templates', templateNames, [], name);
787+
788+ if (result.length === 0) {
789+ !quiet && toastr.warning(`Reasoning template "${name}" not found`);
790+ return '';
791+ }
792+
793+ foundName = result[0].item;
794+ }
795+
796+ UI.$select.val(foundName).trigger('change');
797+ !quiet && toastr.success(`Reasoning template "${foundName}" selected`);
798+ return foundName;
799+
723800}
724801
725802function registerReasoningSlashCommands() {
@@ -853,6 +930,42 @@ function registerReasoningSlashCommands() {
853930 : parsedReasoning.reasoning;
854931 },
855932 }));
933+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
934+ name: 'reasoning-template',
935+ aliases: ['reasoning-formatting', 'reasoning-preset'],
936+ callback: selectReasoningTemplateCallback,
937+ returns: 'template name',
938+ namedArgumentList: [
939+ SlashCommandNamedArgument.fromProps({
940+ name: 'quiet',
941+ description: 'Suppress the toast message on template change',
942+ typeList: [ARGUMENT_TYPE.BOOLEAN],
943+ defaultValue: 'false',
944+ enumList: commonEnumProviders.boolean('trueFalse')(),
945+ }),
946+ ],
947+ unnamedArgumentList: [
948+ SlashCommandArgument.fromProps({
949+ description: 'reasoning template name',
950+ typeList: [ARGUMENT_TYPE.STRING],
951+ enumProvider: () => reasoning_templates.map(x => new SlashCommandEnumValue(x.name, null, enumTypes.enum, enumIcons.preset)),
952+ }),
953+ ],
954+ helpString: `
955+ <div>
956+ Selects a reasoning template by name, using fuzzy search to find the closest match.
957+ Gets the current template if no name is provided.
958+ </div>
959+ <div>
960+ <strong>Example:</strong>
961+ <ul>
962+ <li>
963+ <pre><code class="language-stscript">/reasoning-template DeepSeek</code></pre>
964+ </li>
965+ </ul>
966+ </div>
967+ `,
968+ }));
856969}
857970
858971function registerReasoningMacros() {
@@ -1212,6 +1325,53 @@ function registerReasoningAppEvents() {
12121325 }
12131326}
12141327
1328+/**
1329+ * Loads reasoning templates from the settings data.
1330+ * @param {object} data Settings data
1331+ * @param {ReasoningTemplate[]} data.reasoning Reasoning templates
1332+ * @returns {Promise<void>}
1333+ */
1334+export async function loadReasoningTemplates(data) {
1335+ if (data.reasoning !== undefined) {
1336+ reasoning_templates.splice(0, reasoning_templates.length, ...data.reasoning);
1337+ }
1338+
1339+ for (const template of reasoning_templates) {
1340+ $('<option>').val(template.name).text(template.name).appendTo(UI.$select);
1341+ }
1342+
1343+ // No template name, need to migrate
1344+ if (power_user.reasoning.name === undefined) {
1345+ const defaultTemplate = reasoning_templates.find(p => p.name === DEFAULT_REASONING_TEMPLATE);
1346+ if (defaultTemplate) {
1347+ // If the reasoning settings were modified - migrate them to a custom template
1348+ if (power_user.reasoning.prefix !== defaultTemplate.prefix || power_user.reasoning.suffix !== defaultTemplate.suffix || power_user.reasoning.separator !== defaultTemplate.separator) {
1349+ /** @type {ReasoningTemplate} */
1350+ const data = {
1351+ name: '[Migrated] Custom',
1352+ prefix: power_user.reasoning.prefix,
1353+ suffix: power_user.reasoning.suffix,
1354+ separator: power_user.reasoning.separator,
1355+ };
1356+ await getPresetManager('reasoning')?.savePreset(data.name, data);
1357+ power_user.reasoning.name = data.name;
1358+ } else {
1359+ power_user.reasoning.name = defaultTemplate.name;
1360+ }
1361+ } else {
1362+ // Template not found (deleted or content check skipped - leave blank)
1363+ power_user.reasoning.name = '';
1364+ }
1365+
1366+ saveSettingsDebounced();
1367+ }
1368+
1369+ UI.$select.val(power_user.reasoning.name);
1370+}
1371+
1372+/**
1373+ * Initializes reasoning settings and event handlers.
1374+ */
12151375export function initReasoning() {
12161376 loadReasoningSettings();
12171377 setReasoningEventHandlers();
src/constants.js+1 -0
@@ -43,6 +43,7 @@ export const USER_DIRECTORY_TEMPLATE = Object.freeze({
4343 vectors: 'vectors',
4444 backups: 'backups',
4545 sysprompt: 'sysprompt',
46+ reasoning: 'reasoning',
4647});
4748
4849/**
src/endpoints/content-manager.js+4 -1
@@ -48,6 +48,7 @@ export const CONTENT_TYPES = {
4848 MOVING_UI: 'moving_ui',
4949 QUICK_REPLIES: 'quick_replies',
5050 SYSPROMPT: 'sysprompt',
51+ REASONING: 'reasoning',
5152};
5253
5354/**
@@ -61,7 +62,7 @@ export function getDefaultPresets(directories) {
6162 const presets = [];
6263
6364 for (const contentItem of contentIndex) {
6465 if (contentItem.type.endsWith('_preset') || contentItem.type === ['instruct' || contentItem.type ===, 'context', ||'sysprompt', 'reasoning'].includes(contentItem.type === 'sysprompt')) {
6566 contentItem.name = path.parse(contentItem.filename).name;
6667 contentItem.folder = getTargetByType(contentItem.type, directories);
6768 presets.push(contentItem);
@@ -299,6 +300,8 @@ function getTargetByType(type, directories) {
299300 return directories.quickreplies;
300301 case CONTENT_TYPES.SYSPROMPT:
301302 return directories.sysprompt;
303+ case CONTENT_TYPES.REASONING:
304+ return directories.reasoning;
302305 default:
303306 return null;
304307 }
src/endpoints/presets.js+2 -0
@@ -30,6 +30,8 @@ function getPresetSettingsByAPI(apiId, directories) {
3030 return { folder: directories.context, extension: '.json' };
3131 case 'sysprompt':
3232 return { folder: directories.sysprompt, extension: '.json' };
33+ case 'reasoning':
34+ return { folder: directories.reasoning, extension: '.json' };
3335 default:
3436 return { folder: null, extension: null };
3537 }
src/endpoints/settings.js+2 -0
@@ -254,6 +254,7 @@ router.post('/get', (request, response) => {
254254 const instruct = readAndParseFromDirectory(request.user.directories.instruct);
255255 const context = readAndParseFromDirectory(request.user.directories.context);
256256 const sysprompt = readAndParseFromDirectory(request.user.directories.sysprompt);
257+ const reasoning = readAndParseFromDirectory(request.user.directories.reasoning);
257258
258259 response.send({
259260 settings,
@@ -272,6 +273,7 @@ router.post('/get', (request, response) => {
272273 instruct,
273274 context,
274275 sysprompt,
276+ reasoning,
275277 enable_extensions: ENABLE_EXTENSIONS,
276278 enable_extensions_auto_update: ENABLE_EXTENSIONS_AUTO_UPDATE,
277279 enable_accounts: ENABLE_ACCOUNTS,
src/users.js+1 -0
@@ -95,6 +95,7 @@ const STORAGE_KEYS = {
9595 * @property {string} vectors - The directory where the vectors are stored
9696 * @property {string} backups - The directory where the backups are stored
9797 * @property {string} sysprompt - The directory where the system prompt data is stored
98+ * @property {string} reasoning - The directory where the reasoning templates are stored
9899 */
99100
100101/**