Split Custom OAI prompt post-processing modes

3b4a455ef845ca6682cbd07e6c97be199a8a1399

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

5 files changed, +101 -9Showing whitespace changes
default/config.yaml+2 -0
@@ -121,6 +121,8 @@ extras:
121 speechToTextModel: Xenova/whisper-small121 speechToTextModel: Xenova/whisper-small
122 textToSpeechModel: Xenova/speecht5_tts122 textToSpeechModel: Xenova/speecht5_tts
123# -- OPENAI CONFIGURATION --123# -- OPENAI CONFIGURATION --
124# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message
125promptPlaceholder: "[Start a new chat]"
124openai:126openai:
125 # Will send a random user ID to OpenAI completion API127 # Will send a random user ID to OpenAI completion API
126 randomizeUserId: false128 randomizeUserId: false
public/index.html+2 -1
@@ -3102,7 +3102,8 @@
3102 <h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>3102 <h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>
3103 <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.">3103 <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.">
3104 <option data-i18n="prompt_post_processing_none" value="">None</option>3104 <option data-i18n="prompt_post_processing_none" value="">None</option>
3105 <option value="claude">Claude</option>3105 <option value="merge">Merge consecutive roles</option>
3106 <option value="strict">Strict (user first, alternating roles)</option>
3106 </select>3107 </select>
3107 </form>3108 </form>
3108 <div id="01ai_form" data-source="01ai">3109 <div id="01ai_form" data-source="01ai">
public/scripts/openai.js+7 -0
@@ -199,7 +199,10 @@ const continue_postfix_types = {
199199
200const custom_prompt_post_processing_types = {200const custom_prompt_post_processing_types = {
201 NONE: '',201 NONE: '',
202 /** @deprecated Use MERGE instead. */
202 CLAUDE: 'claude',203 CLAUDE: 'claude',
204 MERGE: 'merge',
205 STRICT: 'strict',
203};206};
204207
205const sensitiveFields = [208const sensitiveFields = [
@@ -3043,6 +3046,10 @@ function loadOpenAISettings(data, settings) {
3043 setNamesBehaviorControls();3046 setNamesBehaviorControls();
3044 setContinuePostfixControls();3047 setContinuePostfixControls();
30453048
3049 if (oai_settings.custom_prompt_post_processing === custom_prompt_post_processing_types.CLAUDE) {
3050 oai_settings.custom_prompt_post_processing = custom_prompt_post_processing_types.MERGE;
3051 }
3052
3046 $('#chat_completion_source').val(oai_settings.chat_completion_source).trigger('change');3053 $('#chat_completion_source').val(oai_settings.chat_completion_source).trigger('change');
3047 $('#oai_max_context_unlocked').prop('checked', oai_settings.max_context_unlocked);3054 $('#oai_max_context_unlocked').prop('checked', oai_settings.max_context_unlocked);
3048 $('#custom_prompt_post_processing').val(oai_settings.custom_prompt_post_processing);3055 $('#custom_prompt_post_processing').val(oai_settings.custom_prompt_post_processing);
src/endpoints/backends/chat-completions.js+6 -3
@@ -4,7 +4,7 @@ const fetch = require('node-fetch').default;
4const { jsonParser } = require('../../express-common');4const { jsonParser } = require('../../express-common');
5const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');5const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');
6const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');6const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
7const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertAI21Messages } = require('../../prompt-converters');7const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertAI21Messages, mergeMessages } = require('../../prompt-converters');
8const CohereStream = require('../../cohere-stream');8const CohereStream = require('../../cohere-stream');
99
10const { readSecret, SECRET_KEYS } = require('../secrets');10const { readSecret, SECRET_KEYS } = require('../secrets');
@@ -31,8 +31,11 @@ const API_AI21 = 'https://api.ai21.com/studio/v1';
31 */31 */
32function postProcessPrompt(messages, type, charName, userName) {32function postProcessPrompt(messages, type, charName, userName) {
33 switch (type) {33 switch (type) {
34 case 'merge':
34 case 'claude':35 case 'claude':
35 return convertClaudeMessages(messages, '', false, '', charName, userName).messages;36 return mergeMessages(messages, charName, userName, false);
37 case 'strict':
38 return mergeMessages(messages, charName, userName, true);
36 default:39 default:
37 return messages;40 return messages;
38 }41 }
@@ -902,7 +905,7 @@ router.post('/generate', jsonParser, function (request, response) {
902 apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);905 apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);
903 headers = {};906 headers = {};
904 bodyParams = {};907 bodyParams = {};
905 request.body.messages = postProcessPrompt(request.body.messages, 'claude', request.body.char_name, request.body.user_name);908 request.body.messages = postProcessPrompt(request.body.messages, 'strict', request.body.char_name, request.body.user_name);
906 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {909 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {
907 apiUrl = API_GROQ;910 apiUrl = API_GROQ;
908 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);911 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
src/prompt-converters.js+84 -5
@@ -1,6 +1,8 @@
1require('./polyfill.js');1require('./polyfill.js');
2const { getConfigValue } = require('./util.js');2const { getConfigValue } = require('./util.js');
33
4const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');
5
4/**6/**
5 * Convert a prompt from the ChatML objects to the format used by Claude.7 * Convert a prompt from the ChatML objects to the format used by Claude.
6 * Mainly deprecated. Only used for counting tokens.8 * Mainly deprecated. Only used for counting tokens.
@@ -122,7 +124,7 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
122 if (messages.length === 0 || (messages.length > 0 && messages[0].role !== 'user')) {124 if (messages.length === 0 || (messages.length > 0 && messages[0].role !== 'user')) {
123 messages.unshift({125 messages.unshift({
124 role: 'user',126 role: 'user',
125 content: humanMsgFix || '[Start a new chat]',127 content: humanMsgFix || PROMPT_PLACEHOLDER,
126 });128 });
127 }129 }
128 }130 }
@@ -260,7 +262,6 @@ function convertCohereMessages(messages, charName = '', userName = '') {
260 'user': 'USER',262 'user': 'USER',
261 'assistant': 'CHATBOT',263 'assistant': 'CHATBOT',
262 };264 };
263 const placeholder = '[Start a new chat]';
264 let systemPrompt = '';265 let systemPrompt = '';
265266
266 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.267 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
@@ -288,12 +289,12 @@ function convertCohereMessages(messages, charName = '', userName = '') {
288 if (messages.length === 0) {289 if (messages.length === 0) {
289 messages.unshift({290 messages.unshift({
290 role: 'user',291 role: 'user',
291 content: placeholder,292 content: PROMPT_PLACEHOLDER,
292 });293 });
293 }294 }
294295
295 const lastNonSystemMessageIndex = messages.findLastIndex(msg => msg.role === 'user' || msg.role === 'assistant');296 const lastNonSystemMessageIndex = messages.findLastIndex(msg => msg.role === 'user' || msg.role === 'assistant');
296 const userPrompt = messages.slice(lastNonSystemMessageIndex).map(msg => msg.content).join('\n\n') || placeholder;297 const userPrompt = messages.slice(lastNonSystemMessageIndex).map(msg => msg.content).join('\n\n') || PROMPT_PLACEHOLDER;
297298
298 const chatHistory = messages.slice(0, lastNonSystemMessageIndex).map(msg => {299 const chatHistory = messages.slice(0, lastNonSystemMessageIndex).map(msg => {
299 return {300 return {
@@ -469,7 +470,7 @@ function convertAI21Messages(messages, charName = '', userName = '') {
469 if (messages.length === 0) {470 if (messages.length === 0) {
470 messages.unshift({471 messages.unshift({
471 role: 'user',472 role: 'user',
472 content: '[Start a new chat]',473 content: PROMPT_PLACEHOLDER,
473 });474 });
474 }475 }
475476
@@ -554,6 +555,83 @@ function convertMistralMessages(messages, charName = '', userName = '') {
554}555}
555556
556/**557/**
558 * Merge messages with the same consecutive role, removing names if they exist.
559 * @param {any[]} messages Messages to merge
560 * @param {string} charName Character name
561 * @param {string} userName User name
562 * @param {boolean} strict Enable strict mode: only allow one system message at the start, force user first message
563 * @returns {any[]} Merged messages
564 */
565function mergeMessages(messages, charName, userName, strict) {
566 let mergedMessages = [];
567
568 // Remove names from the messages
569 messages.forEach((message) => {
570 if (!message.content) {
571 message.content = '';
572 }
573 if (message.role === 'system' && message.name === 'example_assistant') {
574 if (charName && !message.content.startsWith(`${charName}: `)) {
575 message.content = `${charName}: ${message.content}`;
576 }
577 }
578 if (message.role === 'system' && message.name === 'example_user') {
579 if (userName && !message.content.startsWith(`${userName}: `)) {
580 message.content = `${userName}: ${message.content}`;
581 }
582 }
583 if (message.name && message.role !== 'system') {
584 if (!message.content.startsWith(`${message.name}: `)) {
585 message.content = `${message.name}: ${message.content}`;
586 }
587 }
588 if (message.role === 'tool') {
589 message.role = 'user';
590 }
591 delete message.name;
592 delete message.tool_calls;
593 delete message.tool_call_id;
594 });
595
596 // Squash consecutive messages with the same role
597 messages.forEach((message) => {
598 if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role && message.content) {
599 mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
600 } else {
601 mergedMessages.push(message);
602 }
603 });
604
605 // Prevent erroring out if the messages array is empty.
606 if (messages.length === 0) {
607 messages.unshift({
608 role: 'user',
609 content: PROMPT_PLACEHOLDER,
610 });
611 }
612
613 if (strict) {
614 for (let i = 0; i < mergedMessages.length; i++) {
615 // Force mid-prompt system messages to be user messages
616 if (i > 0 && mergedMessages[i].role === 'system') {
617 mergedMessages[i].role = 'user';
618 }
619 }
620 if (mergedMessages.length) {
621 if (mergedMessages[0].role === 'system' && (mergedMessages.length === 1 || mergedMessages[1].role !== 'user')) {
622 mergedMessages.splice(1, 0, { role: 'user', content: PROMPT_PLACEHOLDER });
623 }
624 else if (mergedMessages[0].role !== 'system' && mergedMessages[0].role !== 'user') {
625 mergedMessages.unshift({ role: 'user', content: PROMPT_PLACEHOLDER });
626 }
627 }
628 return mergeMessages(mergedMessages, charName, userName, false);
629 }
630
631 return mergedMessages;
632}
633
634/**
557 * Convert a prompt from the ChatML objects to the format used by Text Completion API.635 * Convert a prompt from the ChatML objects to the format used by Text Completion API.
558 * @param {object[]} messages Array of messages636 * @param {object[]} messages Array of messages
559 * @returns {string} Prompt for Text Completion API637 * @returns {string} Prompt for Text Completion API
@@ -586,4 +664,5 @@ module.exports = {
586 convertCohereMessages,664 convertCohereMessages,
587 convertMistralMessages,665 convertMistralMessages,
588 convertAI21Messages,666 convertAI21Messages,
667 mergeMessages,
589};668};