Add PPP types allowing tool calls (#4125)

a5127a58e65ea9fb3d3a8f0e95adac3298792c35

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

Signed
5 files changed, +94 -44Ignore whitespace
public/index.html+8 -5
@@ -1991,7 +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 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
1995 </div>1995 </div>
1996 </div>1996 </div>
1997 <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">
@@ -3661,10 +3661,13 @@
3661 </h4>3661 </h4>
3662 <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.">3662 <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.">
3663 <option data-i18n="prompt_post_processing_none" value="">None</option>3663 <option data-i18n="prompt_post_processing_none" value="">None</option>
3664 <option data-i18n="prompt_post_processing_merge" value="merge">Merge consecutive roles</option>3664 <option data-i18n="prompt_post_processing_merge" value="merge">Merge consecutive roles (no tools)</option>
3665 <option data-i18n="prompt_post_processing_semi" value="semi">Semi-strict (alternating roles)</option>3665 <option data-i18n="prompt_post_processing_merge_tools" value="merge_tools">Merge consecutive roles (with tools)</option>
3666 <option data-i18n="prompt_post_processing_strict" value="strict">Strict (user first, alternating roles)</option>3666 <option data-i18n="prompt_post_processing_semi" value="semi">Semi-strict (alternating roles; no tools)</option>
3667 <option data-i18n="prompt_post_processing_single" value="single">Single user message</option>3667 <option data-i18n="prompt_post_processing_semi_tools" value="semi_tools">Semi-strict (alternating roles; with tools)</option>
3668 <option data-i18n="prompt_post_processing_strict" value="strict">Strict (user first, alternating roles; no tools)</option>
3669 <option data-i18n="prompt_post_processing_strict_tools" value="strict_tools">Strict (user first, alternating roles; with tools)</option>
3670 <option data-i18n="prompt_post_processing_single" value="single">Single user message (no tools)</option>
3668 </select>3671 </select>
3669 </div>3672 </div>
3670 <div class="flex-container flex">3673 <div class="flex-container flex">
public/scripts/openai.js+3 -0
@@ -207,8 +207,11 @@ export const custom_prompt_post_processing_types = {
207 /** @deprecated Use MERGE instead. */207 /** @deprecated Use MERGE instead. */
208 CLAUDE: 'claude',208 CLAUDE: 'claude',
209 MERGE: 'merge',209 MERGE: 'merge',
210 MERGE_TOOLS: 'merge_tools',
210 SEMI: 'semi',211 SEMI: 'semi',
212 SEMI_TOOLS: 'semi_tools',
211 STRICT: 'strict',213 STRICT: 'strict',
214 STRICT_TOOLS: 'strict_tools',
212 SINGLE: 'single',215 SINGLE: 'single',
213};216};
214217
public/scripts/tool-calling.js+4 -2
@@ -1,7 +1,7 @@
1import { DOMPurify } from '../lib.js';1import { DOMPurify } from '../lib.js';
22
3import { addOneMessage, chat, event_types, eventSource, main_api, saveChatConditional, system_avatar, systemUserName } from '../script.js';3import { addOneMessage, chat, event_types, eventSource, main_api, saveChatConditional, system_avatar, systemUserName } from '../script.js';
4import { chat_completion_sources, model_list, oai_settings } from './openai.js';4import { chat_completion_sources, custom_prompt_post_processing_types, model_list, oai_settings } from './openai.js';
5import { Popup } from './popup.js';5import { Popup } from './popup.js';
6import { SlashCommand } from './slash-commands/SlashCommand.js';6import { SlashCommand } from './slash-commands/SlashCommand.js';
7import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';7import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
@@ -592,7 +592,9 @@ export class ToolManager {
592 }592 }
593593
594 // Post-processing will forcefully remove past tool calls from the prompt, making them useless594 // Post-processing will forcefully remove past tool calls from the prompt, making them useless
595 if (oai_settings.custom_prompt_post_processing) {595 const { NONE, MERGE_TOOLS, SEMI_TOOLS, STRICT_TOOLS } = custom_prompt_post_processing_types;
596 const allowedPromptPostProcessing = [NONE, MERGE_TOOLS, SEMI_TOOLS, STRICT_TOOLS];
597 if (!allowedPromptPostProcessing.includes(oai_settings.custom_prompt_post_processing)) {
596 return false;598 return false;
597 }599 }
598600
src/endpoints/backends/chat-completions.js+6 -31
@@ -26,12 +26,13 @@ import {
26 convertMistralMessages,26 convertMistralMessages,
27 convertAI21Messages,27 convertAI21Messages,
28 convertXAIMessages,28 convertXAIMessages,
29 mergeMessages,
30 cachingAtDepthForOpenRouterClaude,29 cachingAtDepthForOpenRouterClaude,
31 cachingAtDepthForClaude,30 cachingAtDepthForClaude,
32 getPromptNames,31 getPromptNames,
33 calculateClaudeBudgetTokens,32 calculateClaudeBudgetTokens,
34 calculateGoogleBudgetTokens,33 calculateGoogleBudgetTokens,
34 postProcessPrompt,
35 PROMPT_PROCESSING_TYPE,
35} from '../../prompt-converters.js';36} from '../../prompt-converters.js';
3637
37import { readSecret, SECRET_KEYS } from '../secrets.js';38import { readSecret, SECRET_KEYS } from '../secrets.js';
@@ -63,34 +64,6 @@ const API_XAI = 'https://api.x.ai/v1';
63const API_POLLINATIONS = 'https://text.pollinations.ai/openai';64const API_POLLINATIONS = 'https://text.pollinations.ai/openai';
6465
65/**66/**
66 * Applies a post-processing step to the generated messages.
67 * @param {object[]} messages Messages to post-process
68 * @param {string} type Prompt conversion type
69 * @param {import('../../prompt-converters.js').PromptNames} names Prompt names
70 * @returns
71 */
72function postProcessPrompt(messages, type, names) {
73 const addAssistantPrefix = x => x.length && (x[x.length - 1].role !== 'assistant' || (x[x.length - 1].prefix = true)) ? x : x;
74 switch (type) {
75 case 'merge':
76 case 'claude':
77 return mergeMessages(messages, names, { strict: false, placeholders: false, single: false });
78 case 'semi':
79 return mergeMessages(messages, names, { strict: true, placeholders: false, single: false });
80 case 'strict':
81 return mergeMessages(messages, names, { strict: true, placeholders: true, single: false });
82 case 'deepseek':
83 return addAssistantPrefix(mergeMessages(messages, names, { strict: true, placeholders: false, single: false }));
84 case 'deepseek-reasoner':
85 return addAssistantPrefix(mergeMessages(messages, names, { strict: true, placeholders: true, single: false }));
86 case 'single':
87 return mergeMessages(messages, names, { strict: true, placeholders: false, single: true });
88 default:
89 return messages;
90 }
91}
92
93/**
94 * Gets OpenRouter transforms based on the request.67 * Gets OpenRouter transforms based on the request.
95 * @param {import('express').Request} request Express request68 * @param {import('express').Request} request Express request
96 * @returns {string[] | undefined} OpenRouter transforms69 * @returns {string[] | undefined} OpenRouter transforms
@@ -893,7 +866,9 @@ async function sendDeepSeekRequest(request, response) {
893 });866 });
894 }867 }
895868
896 const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';869 const postProcessType = String(request.body.model).endsWith('-reasoner')
870 ? PROMPT_PROCESSING_TYPE.DEEPSEEK_REASONER
871 : PROMPT_PROCESSING_TYPE.DEEPSEEK;
897 const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));872 const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));
898873
899 const requestBody = {874 const requestBody = {
@@ -1404,7 +1379,7 @@ router.post('/generate', function (request, response) {
1404 apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);1379 apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);
1405 headers = {};1380 headers = {};
1406 bodyParams = {};1381 bodyParams = {};
1407 request.body.messages = postProcessPrompt(request.body.messages, 'strict', getPromptNames(request));1382 request.body.messages = postProcessPrompt(request.body.messages, PROMPT_PROCESSING_TYPE.STRICT, getPromptNames(request));
1408 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {1383 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {
1409 apiUrl = API_GROQ;1384 apiUrl = API_GROQ;
1410 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);1385 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
src/prompt-converters.js+73 -6
@@ -12,6 +12,21 @@ const REASONING_EFFORT = {
12 max: 'max',12 max: 'max',
13};13};
1414
15export const PROMPT_PROCESSING_TYPE = {
16 NONE: '',
17 /** @deprecated Use MERGE instead. */
18 CLAUDE: 'claude',
19 MERGE: 'merge',
20 MERGE_TOOLS: 'merge_tools',
21 SEMI: 'semi',
22 SEMI_TOOLS: 'semi_tools',
23 STRICT: 'strict',
24 STRICT_TOOLS: 'strict_tools',
25 SINGLE: 'single',
26 DEEPSEEK: 'deepseek',
27 DEEPSEEK_REASONER: 'deepseek-reasoner',
28};
29
15/**30/**
16 * @typedef {object} PromptNames31 * @typedef {object} PromptNames
17 * @property {string} charName Character name32 * @property {string} charName Character name
@@ -37,6 +52,55 @@ export function getPromptNames(request) {
37}52}
3853
39/**54/**
55 * Adds an assistant prefix to the last message.
56 * @param {any[]} prompt Prompt messages array
57 * @returns {any[]} Transformed messages array
58 */
59function addAssistantPrefix(prompt) {
60 if (!prompt.length) {
61 return prompt;
62 }
63 const hasAnyToolMessages = prompt.some(x => x.role === 'tool');
64 if (!hasAnyToolMessages && prompt[prompt.length - 1].role === 'assistant') {
65 prompt[prompt.length - 1].prefix = true;
66 }
67 return prompt;
68}
69
70/**
71 * Applies a post-processing step to the generated messages.
72 * @param {object[]} messages Messages to post-process
73 * @param {string} type Prompt conversion type
74 * @param {PromptNames} names Prompt names
75 * @returns
76 */
77export function postProcessPrompt(messages, type, names) {
78 switch (type) {
79 case PROMPT_PROCESSING_TYPE.MERGE:
80 case PROMPT_PROCESSING_TYPE.CLAUDE:
81 return mergeMessages(messages, names, { strict: false, placeholders: false, single: false, tools: false });
82 case PROMPT_PROCESSING_TYPE.MERGE_TOOLS:
83 return mergeMessages(messages, names, { strict: false, placeholders: false, single: false, tools: true });
84 case PROMPT_PROCESSING_TYPE.SEMI:
85 return mergeMessages(messages, names, { strict: true, placeholders: false, single: false, tools: false });
86 case PROMPT_PROCESSING_TYPE.SEMI_TOOLS:
87 return mergeMessages(messages, names, { strict: true, placeholders: false, single: false, tools: true });
88 case PROMPT_PROCESSING_TYPE.STRICT:
89 return mergeMessages(messages, names, { strict: true, placeholders: true, single: false, tools: false });
90 case PROMPT_PROCESSING_TYPE.STRICT_TOOLS:
91 return mergeMessages(messages, names, { strict: true, placeholders: true, single: false, tools: true });
92 case PROMPT_PROCESSING_TYPE.DEEPSEEK:
93 return addAssistantPrefix(mergeMessages(messages, names, { strict: true, placeholders: false, single: false, tools: true }));
94 case PROMPT_PROCESSING_TYPE.DEEPSEEK_REASONER:
95 return addAssistantPrefix(mergeMessages(messages, names, { strict: true, placeholders: true, single: false, tools: true }));
96 case PROMPT_PROCESSING_TYPE.SINGLE:
97 return mergeMessages(messages, names, { strict: true, placeholders: false, single: true, tools: false });
98 default:
99 return messages;
100 }
101}
102
103/**
40 * Convert a prompt from the ChatML objects to the format used by Claude.104 * Convert a prompt from the ChatML objects to the format used by Claude.
41 * Mainly deprecated. Only used for counting tokens.105 * Mainly deprecated. Only used for counting tokens.
42 * @param {object[]} messages Array of messages106 * @param {object[]} messages Array of messages
@@ -712,9 +776,10 @@ export function convertXAIMessages(messages, names) {
712 * @param {boolean} [options.strict] Enable strict mode: only allow one system message at the start, force user first message776 * @param {boolean} [options.strict] Enable strict mode: only allow one system message at the start, force user first message
713 * @param {boolean} [options.placeholders] Add user placeholders to the messages in strict mode777 * @param {boolean} [options.placeholders] Add user placeholders to the messages in strict mode
714 * @param {boolean} [options.single] Force every role to be user, merging all messages into one778 * @param {boolean} [options.single] Force every role to be user, merging all messages into one
779 * @param {boolean} [options.tools] Allow tool calls in the prompt. If false, tool call messages are removed.
715 * @returns {any[]} Merged messages780 * @returns {any[]} Merged messages
716 */781 */
717export function mergeMessages(messages, names, { strict = false, placeholders = false, single = false } = {}) {782export function mergeMessages(messages, names, { strict = false, placeholders = false, single = false, tools = false } = {}) {
718 let mergedMessages = [];783 let mergedMessages = [];
719784
720 /** @type {Map<string,object>} */785 /** @type {Map<string,object>} */
@@ -756,7 +821,7 @@ export function mergeMessages(messages, names, { strict = false, placeholders =
756 message.content = `${message.name}: ${message.content}`;821 message.content = `${message.name}: ${message.content}`;
757 }822 }
758 }823 }
759 if (message.role === 'tool') {824 if (message.role === 'tool' && !tools) {
760 message.role = 'user';825 message.role = 'user';
761 }826 }
762 if (single) {827 if (single) {
@@ -774,13 +839,15 @@ export function mergeMessages(messages, names, { strict = false, placeholders =
774 message.role = 'user';839 message.role = 'user';
775 }840 }
776 delete message.name;841 delete message.name;
777 delete message.tool_calls;842 if (!tools) {
778 delete message.tool_call_id;843 delete message.tool_calls;
844 delete message.tool_call_id;
845 }
779 });846 });
780847
781 // Squash consecutive messages with the same role848 // Squash consecutive messages with the same role
782 messages.forEach((message) => {849 messages.forEach((message) => {
783 if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role && message.content) {850 if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role && message.content && message.role !== 'tool') {
784 mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;851 mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
785 } else {852 } else {
786 mergedMessages.push(message);853 mergedMessages.push(message);
@@ -836,7 +903,7 @@ export function mergeMessages(messages, names, { strict = false, placeholders =
836 mergedMessages.unshift({ role: 'user', content: PROMPT_PLACEHOLDER });903 mergedMessages.unshift({ role: 'user', content: PROMPT_PLACEHOLDER });
837 }904 }
838 }905 }
839 return mergeMessages(mergedMessages, names, { strict: false, placeholders, single: false });906 return mergeMessages(mergedMessages, names, { strict: false, placeholders, single: false, tools });
840 }907 }
841908
842 return mergedMessages;909 return mergedMessages;