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 @@
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>
19941994 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
19951995 </div>
19961996 </div>
19971997 <div class="range-block" data-source="openai,openrouter,mistralai,makersuite,vertexai,claude,custom,01ai,xai,pollinations">
@@ -3661,10 +3661,13 @@
36613661 </h4>
36623662 <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.">
36633663 <option data-i18n="prompt_post_processing_none" value="">None</option>
36643664 <option data-i18n="prompt_post_processing_merge" value="merge">Merge consecutive roles (no tools)</option>
36653665 <option data-i18n="prompt_post_processing_semiprompt_post_processing_merge_tools" value="semimerge_tools">Semi-strictMerge (alternatingconsecutive roles (with tools)</option>
36663666 <option data-i18n="prompt_post_processing_strictprompt_post_processing_semi" value="strictsemi">StrictSemi-strict (user first, alternating roles; no tools)</option>
36673667 <option data-i18n="prompt_post_processing_singleprompt_post_processing_semi_tools" value="singlesemi_tools">SingleSemi-strict user(alternating messageroles; 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>
36683671 </select>
36693672 </div>
36703673 <div class="flex-container flex">
public/scripts/openai.js+3 -0
@@ -207,8 +207,11 @@ export const custom_prompt_post_processing_types = {
207207 /** @deprecated Use MERGE instead. */
208208 CLAUDE: 'claude',
209209 MERGE: 'merge',
210+ MERGE_TOOLS: 'merge_tools',
210211 SEMI: 'semi',
212+ SEMI_TOOLS: 'semi_tools',
211213 STRICT: 'strict',
214+ STRICT_TOOLS: 'strict_tools',
212215 SINGLE: 'single',
213216};
214217
public/scripts/tool-calling.js+4 -2
@@ -1,7 +1,7 @@
11import { DOMPurify } from '../lib.js';
22
33import { addOneMessage, chat, event_types, eventSource, main_api, saveChatConditional, system_avatar, systemUserName } from '../script.js';
44import { chat_completion_sources, custom_prompt_post_processing_types, model_list, oai_settings } from './openai.js';
55import { Popup } from './popup.js';
66import { SlashCommand } from './slash-commands/SlashCommand.js';
77import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
@@ -592,7 +592,9 @@ export class ToolManager {
592592 }
593593
594594 // 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)) {
596598 return false;
597599 }
598600
src/endpoints/backends/chat-completions.js+6 -31
@@ -26,12 +26,13 @@ import {
2626 convertMistralMessages,
2727 convertAI21Messages,
2828 convertXAIMessages,
29- mergeMessages,
3029 cachingAtDepthForOpenRouterClaude,
3130 cachingAtDepthForClaude,
3231 getPromptNames,
3332 calculateClaudeBudgetTokens,
3433 calculateGoogleBudgetTokens,
34+ postProcessPrompt,
35+ PROMPT_PROCESSING_TYPE,
3536} from '../../prompt-converters.js';
3637
3738import { readSecret, SECRET_KEYS } from '../secrets.js';
@@ -63,34 +64,6 @@ const API_XAI = 'https://api.x.ai/v1';
6364const API_POLLINATIONS = 'https://text.pollinations.ai/openai';
6465
6566/**
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- */
72-function 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-/**
9467 * Gets OpenRouter transforms based on the request.
9568 * @param {import('express').Request} request Express request
9669 * @returns {string[] | undefined} OpenRouter transforms
@@ -893,7 +866,9 @@ async function sendDeepSeekRequest(request, response) {
893866 });
894867 }
895868
896869 const postProcessType = String(request.body.model).endsWith('-reasoner') ? 'deepseek-reasoner' : 'deepseek';
870+ ? PROMPT_PROCESSING_TYPE.DEEPSEEK_REASONER
871+ : PROMPT_PROCESSING_TYPE.DEEPSEEK;
897872 const processedMessages = postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request));
898873
899874 const requestBody = {
@@ -1404,7 +1379,7 @@ router.post('/generate', function (request, response) {
14041379 apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);
14051380 headers = {};
14061381 bodyParams = {};
14071382 request.body.messages = postProcessPrompt(request.body.messages, 'strict'PROMPT_PROCESSING_TYPE.STRICT, getPromptNames(request));
14081383 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {
14091384 apiUrl = API_GROQ;
14101385 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
src/prompt-converters.js+73 -6
@@ -12,6 +12,21 @@ const REASONING_EFFORT = {
1212 max: 'max',
1313};
1414
15+export 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+
1530/**
1631 * @typedef {object} PromptNames
1732 * @property {string} charName Character name
@@ -37,6 +52,55 @@ export function getPromptNames(request) {
3752}
3853
3954/**
55+ * Adds an assistant prefix to the last message.
56+ * @param {any[]} prompt Prompt messages array
57+ * @returns {any[]} Transformed messages array
58+ */
59+function 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+ */
77+export 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+/**
40104 * Convert a prompt from the ChatML objects to the format used by Claude.
41105 * Mainly deprecated. Only used for counting tokens.
42106 * @param {object[]} messages Array of messages
@@ -712,9 +776,10 @@ export function convertXAIMessages(messages, names) {
712776 * @param {boolean} [options.strict] Enable strict mode: only allow one system message at the start, force user first message
713777 * @param {boolean} [options.placeholders] Add user placeholders to the messages in strict mode
714778 * @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.
715780 * @returns {any[]} Merged messages
716781 */
717782export function mergeMessages(messages, names, { strict = false, placeholders = false, single = false, tools = false } = {}) {
718783 let mergedMessages = [];
719784
720785 /** @type {Map<string,object>} */
@@ -756,7 +821,7 @@ export function mergeMessages(messages, names, { strict = false, placeholders =
756821 message.content = `${message.name}: ${message.content}`;
757822 }
758823 }
759824 if (message.role === 'tool' && !tools) {
760825 message.role = 'user';
761826 }
762827 if (single) {
@@ -774,13 +839,15 @@ export function mergeMessages(messages, names, { strict = false, placeholders =
774839 message.role = 'user';
775840 }
776841 delete message.name;
777- delete message.tool_calls;
842+ if (!tools) {
778843 delete message.tool_call_idtool_calls;
844+ delete message.tool_call_id;
845+ }
779846 });
780847
781848 // Squash consecutive messages with the same role
782849 messages.forEach((message) => {
783850 if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role && message.content && message.role !== 'tool') {
784851 mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
785852 } else {
786853 mergedMessages.push(message);
@@ -836,7 +903,7 @@ export function mergeMessages(messages, names, { strict = false, placeholders =
836903 mergedMessages.unshift({ role: 'user', content: PROMPT_PLACEHOLDER });
837904 }
838905 }
839906 return mergeMessages(mergedMessages, names, { strict: false, placeholders, single: false, tools });
840907 }
841908
842909 return mergedMessages;