Allow prompt post-processing for all sources. Add 'single user msg' processing (#4009) * Allow prompt post-processing for all sources. Add 'single user msg' PPP type * Fix copilot comments * Fix typo in element id * Remove redundant conditions * Lint fix * Add link to PPP docs

ade45b6cd18dc6c525349dad88c83c7533ba6d4b

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

Signed
5 files changed, +55 -22Ignore whitespace
public/index.html+11 -2
@@ -1991,6 +1991,7 @@
1991 <div class="flexBasis100p toggle-description justifyLeft">1991 <div class="flexBasis100p toggle-description justifyLeft">
1992 <span data-i18n="enable_functions_desc_1">Allows using </span><a href="https://platform.openai.com/docs/guides/function-calling" target="_blank" data-i18n="enable_functions_desc_2">function tools</a>.1992 <span data-i18n="enable_functions_desc_1">Allows using </span><a href="https://platform.openai.com/docs/guides/function-calling" target="_blank" data-i18n="enable_functions_desc_2">function tools</a>.
1993 <span data-i18n="enable_functions_desc_3">Can be utilized by various extensions to provide additional functionality.</span>1993 <span data-i18n="enable_functions_desc_3">Can be utilized by various extensions to provide additional functionality.</span>
1994 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing is used!</strong>
1994 </div>1995 </div>
1995 </div>1996 </div>
1996 <div class="range-block" data-source="openai,openrouter,mistralai,makersuite,vertexai,claude,custom,01ai,xai,pollinations">1997 <div class="range-block" data-source="openai,openrouter,mistralai,makersuite,vertexai,claude,custom,01ai,xai,pollinations">
@@ -3535,13 +3536,21 @@
3535 </span>3536 </span>
3536 </div>3537 </div>
3537 </div>3538 </div>
3538 <div id="prompt_post_porcessing_form" data-source="custom,openrouter">3539 <div id="prompt_post_processing_form">
3539 <h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>3540 <h4>
3541 <span data-i18n="Prompt Post-Processing">
3542 Prompt Post-Processing
3543 </span>
3544 <a href="https://docs.sillytavern.app/usage/api-connections/openai/#prompt-post-processing" class="notes-link" target="_blank">
3545 <span class="fa-solid fa-circle-question note-link-span"></span>
3546 </a>
3547 </h4>
3540 <select id="custom_prompt_post_processing" class="text_pole" title="Applies additional processing to the prompt before sending it to the API." data-i18n="[title]Applies additional processing to the prompt before sending it to the API.">3548 <select id="custom_prompt_post_processing" class="text_pole" title="Applies additional processing to the prompt before sending it to the API." data-i18n="[title]Applies additional processing to the prompt before sending it to the API.">
3541 <option data-i18n="prompt_post_processing_none" value="">None</option>3549 <option data-i18n="prompt_post_processing_none" value="">None</option>
3542 <option data-i18n="prompt_post_processing_merge" value="merge">Merge consecutive roles</option>3550 <option data-i18n="prompt_post_processing_merge" value="merge">Merge consecutive roles</option>
3543 <option data-i18n="prompt_post_processing_semi" value="semi">Semi-strict (alternating roles)</option>3551 <option data-i18n="prompt_post_processing_semi" value="semi">Semi-strict (alternating roles)</option>
3544 <option data-i18n="prompt_post_processing_strict" value="strict">Strict (user first, alternating roles)</option>3552 <option data-i18n="prompt_post_processing_strict" value="strict">Strict (user first, alternating roles)</option>
3553 <option data-i18n="prompt_post_processing_single" value="single">Single user message</option>
3545 </select>3554 </select>
3546 </div>3555 </div>
3547 <div class="flex-container flex">3556 <div class="flex-container flex">
public/scripts/openai.js+2 -1
@@ -203,13 +203,14 @@ const continue_postfix_types = {
203 DOUBLE_NEWLINE: '\n\n',203 DOUBLE_NEWLINE: '\n\n',
204};204};
205205
206const custom_prompt_post_processing_types = {206export const custom_prompt_post_processing_types = {
207 NONE: '',207 NONE: '',
208 /** @deprecated Use MERGE instead. */208 /** @deprecated Use MERGE instead. */
209 CLAUDE: 'claude',209 CLAUDE: 'claude',
210 MERGE: 'merge',210 MERGE: 'merge',
211 SEMI: 'semi',211 SEMI: 'semi',
212 STRICT: 'strict',212 STRICT: 'strict',
213 SINGLE: 'single',
213};214};
214215
215const openrouter_middleout_types = {216const openrouter_middleout_types = {
public/scripts/tool-calling.js+5 -0
@@ -575,6 +575,11 @@ export class ToolManager {
575 return false;575 return false;
576 }576 }
577577
578 // Post-processing will forcefully remove past tool calls from the prompt, making them useless
579 if (oai_settings.custom_prompt_post_processing) {
580 return false;
581 }
582
578 if (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS && Array.isArray(model_list)) {583 if (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS && Array.isArray(model_list)) {
579 const currentModel = model_list.find(model => model.id === oai_settings.pollinations_model);584 const currentModel = model_list.find(model => model.id === oai_settings.pollinations_model);
580 if (currentModel) {585 if (currentModel) {
src/endpoints/backends/chat-completions.js+17 -15
@@ -72,15 +72,17 @@ function postProcessPrompt(messages, type, names) {
72 switch (type) {72 switch (type) {
73 case 'merge':73 case 'merge':
74 case 'claude':74 case 'claude':
75 return mergeMessages(messages, names, false, false);75 return mergeMessages(messages, names, { strict: false, placeholders: false, single: false });
76 case 'semi':76 case 'semi':
77 return mergeMessages(messages, names, true, false);77 return mergeMessages(messages, names, { strict: true, placeholders: false, single: false });
78 case 'strict':78 case 'strict':
79 return mergeMessages(messages, names, true, true);79 return mergeMessages(messages, names, { strict: true, placeholders: true, single: false });
80 case 'deepseek':80 case 'deepseek':
81 return addAssistantPrefix(mergeMessages(messages, names, true, false));81 return addAssistantPrefix(mergeMessages(messages, names, { strict: true, placeholders: false, single: false }));
82 case 'deepseek-reasoner':82 case 'deepseek-reasoner':
83 return addAssistantPrefix(mergeMessages(messages, names, true, true));83 return addAssistantPrefix(mergeMessages(messages, names, { strict: true, placeholders: true, single: false }));
84 case 'single':
85 return mergeMessages(messages, names, { strict: true, placeholders: false, single: true });
84 default:86 default:
85 return messages;87 return messages;
86 }88 }
@@ -383,7 +385,7 @@ async function sendMakerSuiteRequest(request, response) {
383385
384 function getGeminiBody() {386 function getGeminiBody() {
385 // #region UGLY MODEL LISTS AREA387 // #region UGLY MODEL LISTS AREA
386 const imageGenerationModels = [388 const imageGenerationModels = [
387 'gemini-2.0-flash-exp',389 'gemini-2.0-flash-exp',
388 'gemini-2.0-flash-exp-image-generation',390 'gemini-2.0-flash-exp-image-generation',
389 ];391 ];
@@ -1206,6 +1208,15 @@ router.post('/bias', async function (request, response) {
1206router.post('/generate', function (request, response) {1208router.post('/generate', function (request, response) {
1207 if (!request.body) return response.status(400).send({ error: true });1209 if (!request.body) return response.status(400).send({ error: true });
12081210
1211 const postProcessingType = request.body.custom_prompt_post_processing;
1212 if (Array.isArray(request.body.messages) && postProcessingType) {
1213 console.info('Applying custom prompt post-processing of type', postProcessingType);
1214 request.body.messages = postProcessPrompt(
1215 request.body.messages,
1216 postProcessingType,
1217 getPromptNames(request));
1218 }
1219
1209 switch (request.body.chat_completion_source) {1220 switch (request.body.chat_completion_source) {
1210 case CHAT_COMPLETION_SOURCES.CLAUDE: return sendClaudeRequest(request, response);1221 case CHAT_COMPLETION_SOURCES.CLAUDE: return sendClaudeRequest(request, response);
1211 case CHAT_COMPLETION_SOURCES.SCALE: return sendScaleRequest(request, response);1222 case CHAT_COMPLETION_SOURCES.SCALE: return sendScaleRequest(request, response);
@@ -1224,15 +1235,6 @@ router.post('/generate', function (request, response) {
1224 let bodyParams;1235 let bodyParams;
1225 const isTextCompletion = Boolean(request.body.model && TEXT_COMPLETION_MODELS.includes(request.body.model)) || typeof request.body.messages === 'string';1236 const isTextCompletion = Boolean(request.body.model && TEXT_COMPLETION_MODELS.includes(request.body.model)) || typeof request.body.messages === 'string';
12261237
1227 const postProcessTypes = [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENROUTER];
1228 if (Array.isArray(request.body.messages) && postProcessTypes.includes(request.body.chat_completion_source) && request.body.custom_prompt_post_processing) {
1229 console.info('Applying custom prompt post-processing of type', request.body.custom_prompt_post_processing);
1230 request.body.messages = postProcessPrompt(
1231 request.body.messages,
1232 request.body.custom_prompt_post_processing,
1233 getPromptNames(request));
1234 }
1235
1236 if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {1238 if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {
1237 apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString();1239 apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString();
1238 apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI);1240 apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI);
src/prompt-converters.js+20 -4
@@ -695,11 +695,13 @@ export function convertXAIMessages(messages, names) {
695 * Merge messages with the same consecutive role, removing names if they exist.695 * Merge messages with the same consecutive role, removing names if they exist.
696 * @param {any[]} messages Messages to merge696 * @param {any[]} messages Messages to merge
697 * @param {PromptNames} names Prompt names697 * @param {PromptNames} names Prompt names
698 * @param {boolean} strict Enable strict mode: only allow one system message at the start, force user first message698 * @param {object} options Options for merging
699 * @param {boolean} placeholders Add user placeholders to the messages in strict mode699 * @param {boolean} [options.strict] Enable strict mode: only allow one system message at the start, force user first message
700 * @param {boolean} [options.placeholders] Add user placeholders to the messages in strict mode
701 * @param {boolean} [options.single] Force every role to be user, merging all messages into one
700 * @returns {any[]} Merged messages702 * @returns {any[]} Merged messages
701 */703 */
702export function mergeMessages(messages, names, strict, placeholders) {704export function mergeMessages(messages, names, { strict = false, placeholders = false, single = false } = {}) {
703 let mergedMessages = [];705 let mergedMessages = [];
704706
705 /** @type {Map<string,object>} */707 /** @type {Map<string,object>} */
@@ -744,6 +746,20 @@ export function mergeMessages(messages, names, strict, placeholders) {
744 if (message.role === 'tool') {746 if (message.role === 'tool') {
745 message.role = 'user';747 message.role = 'user';
746 }748 }
749 if (single) {
750 if (message.role === 'assistant') {
751 if (names.charName && !message.content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(message.content)) {
752 message.content = `${names.charName}: ${message.content}`;
753 }
754 }
755 if (message.role === 'user') {
756 if (names.userName && !message.content.startsWith(`${names.userName}: `)) {
757 message.content = `${names.userName}: ${message.content}`;
758 }
759 }
760
761 message.role = 'user';
762 }
747 delete message.name;763 delete message.name;
748 delete message.tool_calls;764 delete message.tool_calls;
749 delete message.tool_call_id;765 delete message.tool_call_id;
@@ -807,7 +823,7 @@ export function mergeMessages(messages, names, strict, placeholders) {
807 mergedMessages.unshift({ role: 'user', content: PROMPT_PLACEHOLDER });823 mergedMessages.unshift({ role: 'user', content: PROMPT_PLACEHOLDER });
808 }824 }
809 }825 }
810 return mergeMessages(mergedMessages, names, false, placeholders);826 return mergeMessages(mergedMessages, names, { strict: false, placeholders, single: false });
811 }827 }
812828
813 return mergedMessages;829 return mergedMessages;