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 @@
19911991 <div class="flexBasis100p toggle-description justifyLeft">
19921992 <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>.
19931993 <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>
19941995 </div>
19951996 </div>
19961997 <div class="range-block" data-source="openai,openrouter,mistralai,makersuite,vertexai,claude,custom,01ai,xai,pollinations">
@@ -3535,13 +3536,21 @@
35353536 </span>
35363537 </div>
35373538 </div>
35383539 <div id="prompt_post_porcessing_form" data-source="custom,openrouterprompt_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>
35403548 <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.">
35413549 <option data-i18n="prompt_post_processing_none" value="">None</option>
35423550 <option data-i18n="prompt_post_processing_merge" value="merge">Merge consecutive roles</option>
35433551 <option data-i18n="prompt_post_processing_semi" value="semi">Semi-strict (alternating roles)</option>
35443552 <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>
35453554 </select>
35463555 </div>
35473556 <div class="flex-container flex">
public/scripts/openai.js+2 -1
@@ -203,13 +203,14 @@ const continue_postfix_types = {
203203 DOUBLE_NEWLINE: '\n\n',
204204};
205205
206206export const custom_prompt_post_processing_types = {
207207 NONE: '',
208208 /** @deprecated Use MERGE instead. */
209209 CLAUDE: 'claude',
210210 MERGE: 'merge',
211211 SEMI: 'semi',
212212 STRICT: 'strict',
213+ SINGLE: 'single',
213214};
214215
215216const openrouter_middleout_types = {
public/scripts/tool-calling.js+5 -0
@@ -575,6 +575,11 @@ export class ToolManager {
575575 return false;
576576 }
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+
578583 if (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS && Array.isArray(model_list)) {
579584 const currentModel = model_list.find(model => model.id === oai_settings.pollinations_model);
580585 if (currentModel) {
src/endpoints/backends/chat-completions.js+17 -15
@@ -72,15 +72,17 @@ function postProcessPrompt(messages, type, names) {
7272 switch (type) {
7373 case 'merge':
7474 case 'claude':
7575 return mergeMessages(messages, names, { strict: false, placeholders: false, single: false });
7676 case 'semi':
7777 return mergeMessages(messages, names, { strict: true, placeholders: false, single: false });
7878 case 'strict':
7979 return mergeMessages(messages, names, { strict: true, placeholders: true, single: false });
8080 case 'deepseek':
8181 return addAssistantPrefix(mergeMessages(messages, names, { strict: true, placeholders: false, single: false }));
8282 case 'deepseek-reasoner':
8383 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 });
8486 default:
8587 return messages;
8688 }
@@ -383,7 +385,7 @@ async function sendMakerSuiteRequest(request, response) {
383385
384386 function getGeminiBody() {
385387 // #region UGLY MODEL LISTS AREA
386388 const imageGenerationModels = [
387389 'gemini-2.0-flash-exp',
388390 'gemini-2.0-flash-exp-image-generation',
389391 ];
@@ -1206,6 +1208,15 @@ router.post('/bias', async function (request, response) {
12061208router.post('/generate', function (request, response) {
12071209 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+
12091220 switch (request.body.chat_completion_source) {
12101221 case CHAT_COMPLETION_SOURCES.CLAUDE: return sendClaudeRequest(request, response);
12111222 case CHAT_COMPLETION_SOURCES.SCALE: return sendScaleRequest(request, response);
@@ -1224,15 +1235,6 @@ router.post('/generate', function (request, response) {
12241235 let bodyParams;
12251236 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-
12361238 if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {
12371239 apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString();
12381240 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) {
695695 * Merge messages with the same consecutive role, removing names if they exist.
696696 * @param {any[]} messages Messages to merge
697697 * @param {PromptNames} names Prompt names
698- * @param {boolean} strict Enable strict mode: only allow one system message at the start, force user first message
698+ * @param {object} options Options for merging
699699 * @param {boolean} placeholders[options.strict] AddEnable userstrict placeholdersmode: toonly allow one system message at the messagesstart, inforce strictuser modefirst 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
700702 * @returns {any[]} Merged messages
701703 */
702704export function mergeMessages(messages, names, { strict = false, placeholders = false, single = false } = {}) {
703705 let mergedMessages = [];
704706
705707 /** @type {Map<string,object>} */
@@ -744,6 +746,20 @@ export function mergeMessages(messages, names, strict, placeholders) {
744746 if (message.role === 'tool') {
745747 message.role = 'user';
746748 }
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+ }
747763 delete message.name;
748764 delete message.tool_calls;
749765 delete message.tool_call_id;
@@ -807,7 +823,7 @@ export function mergeMessages(messages, names, strict, placeholders) {
807823 mergedMessages.unshift({ role: 'user', content: PROMPT_PLACEHOLDER });
808824 }
809825 }
810826 return mergeMessages(mergedMessages, names, { strict: false, placeholders, single: false });
811827 }
812828
813829 return mergedMessages;