Merge branch 'staging' into feat/AdditionalLogins

5765ea8500f7ac99e8b03dba538468f0652a5ae4

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

32 files changed, +1584 -506Showing whitespace changes
.eslintrc.js+11 -8
@@ -59,15 +59,18 @@ module.exports = {
5959 },
6060 },
6161 ],
62- // There are various vendored libraries that shouldn't be linted
6362 ignorePatterns: [
6463 'public/lib/**/node_modules/**',
6564 '*.min.js*/dist/**',
6665 'src/ai_horde/**/.git/**',
6766 'pluginspublic/**lib/**',
6867 'databackups/**/*',
6968 'backupsdata/**/*',
7069 'node_modulescache/**/*',
70+ 'src/tokenizers/**',
71+ 'docker/**',
72+ 'plugins/**',
73+ '**/*.min.js',
7174 ],
7275 rules: {
7376 'no-unused-vars': ['error', { args: 'none' }],
default/config.yaml+2 -0
@@ -134,6 +134,8 @@ extras:
134134 speechToTextModel: Xenova/whisper-small
135135 textToSpeechModel: Xenova/speecht5_tts
136136# -- OPENAI CONFIGURATION --
137+# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message
138+promptPlaceholder: "[Start a new chat]"
137139openai:
138140 # Will send a random user ID to OpenAI completion API
139141 randomizeUserId: false
jsconfig.json+9 -10
@@ -11,15 +11,14 @@
1111 "resolveJsonModule": true
1212 },
1313 "exclude": [
1414 "**/node_modules/**",
1515 "**/node_modulesdist/**",
1616 "public**/lib.git/**",
1717 "backupspublic/lib/**",
1818 "databackups/**",
1919 "**/distdata/**",
2020 "distcache/**",
2121 "cachesrc/tokenizers/**",
2222 "src/tokenizersdocker/**",
23- "docker/*",
2423 ]
2524}
package-lock.json+34 -12
@@ -59,9 +59,10 @@
5959 "sillytavern": "server.js"
6060 },
6161 "devDependencies": {
62+ "@types/dompurify": "^3.0.5",
6263 "@types/jquery": "^3.5.29",
6364 "eslint@types/toastr": "^82.571.043",
6465 "jqueryeslint": "^38.657.40"
6566 },
6667 "engines": {
6768 "node": ">= 18"
@@ -960,6 +961,16 @@
960961 "@types/responselike": "^1.0.0"
961962 }
962963 },
964+ "node_modules/@types/dompurify": {
965+ "version": "3.0.5",
966+ "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
967+ "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
968+ "dev": true,
969+ "license": "MIT",
970+ "dependencies": {
971+ "@types/trusted-types": "*"
972+ }
973+ },
963974 "node_modules/@types/http-cache-semantics": {
964975 "version": "4.0.2",
965976 "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz",
@@ -967,10 +978,11 @@
967978 "license": "MIT"
968979 },
969980 "node_modules/@types/jquery": {
970981 "version": "3.5.2931",
971982 "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.2931.tgz",
972983 "integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIgrf/Oy8FAbzSZtFHRjc7IpbfFVmpLtJiB+UOoywpRsuO5JxjybyegcPJ/YZfMwr+FVuQbm7IaWC4y3FVYfVDxRGqmUCFjjPII0HWaP0vTPJGp6m4o13AXySCcMbWfrWtBFAKw==",
973984 "dev": true,
985+ "license": "MIT",
974986 "dependencies": {
975987 "@types/sizzle": "*"
976988 }
@@ -1021,6 +1033,23 @@
10211033 "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
10221034 "dev": true
10231035 },
1036+ "node_modules/@types/toastr": {
1037+ "version": "2.1.43",
1038+ "resolved": "https://registry.npmjs.org/@types/toastr/-/toastr-2.1.43.tgz",
1039+ "integrity": "sha512-sLC2fr2OXeE1iyhUixpQ64wQ2tA26awmLidn4tXTLBz4yP/VhtYUKHpmiIyDtztKkHjucdiTLH8F5uRRyhNi2Q==",
1040+ "dev": true,
1041+ "license": "MIT",
1042+ "dependencies": {
1043+ "@types/jquery": "*"
1044+ }
1045+ },
1046+ "node_modules/@types/trusted-types": {
1047+ "version": "2.0.7",
1048+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1049+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1050+ "dev": true,
1051+ "license": "MIT"
1052+ },
10241053 "node_modules/@ungap/structured-clone": {
10251054 "version": "1.2.0",
10261055 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -3881,13 +3910,6 @@
38813910 "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
38823911 "license": "BSD-3-Clause"
38833912 },
3884- "node_modules/jquery": {
3885- "version": "3.7.0",
3886- "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
3887- "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
3888- "dev": true,
3889- "license": "MIT"
3890- },
38913913 "node_modules/js-yaml": {
38923914 "version": "4.1.0",
38933915 "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
package.json+3 -2
@@ -85,8 +85,9 @@
8585 },
8686 "main": "server.js",
8787 "devDependencies": {
88+ "@types/dompurify": "^3.0.5",
8889 "@types/jquery": "^3.5.29",
8990 "eslint@types/toastr": "^82.571.043",
9091 "jqueryeslint": "^38.657.40"
9192 }
9293}
public/css/select2-overrides.css+2 -1
@@ -17,7 +17,7 @@
1717
1818.select2-container .select2-selection .select2-selection__clear {
1919 color: var(--SmartThemeBodyColor);
2020 font-size: 24px20px;
2121 padding: 0;
2222 position: absolute;
2323 right: 5px;
@@ -50,6 +50,7 @@
5050 padding: revert;
5151 border-right: 1px solid var(--SmartThemeBorderColor);
5252 font-size: 1.1em;
53+ line-height: 1;
5354}
5455
5556.select2-container .select2-selection--multiple .select2-selection__choice__display {
public/css/st-tailwind.css+4 -0
@@ -612,3 +612,7 @@ ul.li-padding-bot5 li {
612612ul.li-padding-bot10 li {
613613 padding-bottom: 10px;
614614}
615+
616+.wordBreakAll {
617+ word-break: break-all;
618+}
public/css/toggle-dependent.css+11 -0
@@ -463,3 +463,14 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
463463label[for="trim_spaces"]:has(input:checked) i.warning {
464464 display: none;
465465}
466+
467+#claude_function_prefill_warning {
468+ display: none;
469+}
470+
471+#openai_settings:has(#openai_function_calling:checked):has(#claude_assistant_prefill:not(:placeholder-shown), #claude_assistant_impersonation:not(:placeholder-shown)) #claude_function_prefill_warning {
472+ display: flex;
473+ align-items: center;
474+ gap: 5px;
475+ margin: 10px 0;
476+}
public/global.d.ts+0 -42
@@ -1,5 +1,4 @@
11// Global namespace modules
2-declare var DOMPurify;
32declare var droll;
43declare var Handlebars;
54declare var hljs;
@@ -1365,44 +1364,3 @@ declare namespace moment {
13651364declare global {
13661365 const moment: typeof moment;
13671366}
1368-
1369-/**
1370- * Callback data for the `LLM_FUNCTION_TOOL_REGISTER` event type that is triggered when a function tool can be registered.
1371- */
1372-interface FunctionToolRegister {
1373- /**
1374- * The type of generation that is being used
1375- */
1376- type?: string;
1377- /**
1378- * Generation data, including messages and sampling parameters
1379- */
1380- data: Record<string, object>;
1381- /**
1382- * Callback to register an LLM function tool.
1383- */
1384- registerFunctionTool: typeof registerFunctionTool;
1385-}
1386-
1387-/**
1388- * Callback data for the `LLM_FUNCTION_TOOL_REGISTER` event type that is triggered when a function tool is registered.
1389- * @param name Name of the function tool to register
1390- * @param description Description of the function tool
1391- * @param params JSON schema for the parameters of the function tool
1392- * @param required Whether the function tool should be forced to be used
1393- */
1394-declare function registerFunctionTool(name: string, description: string, params: object, required: boolean): Promise<void>;
1395-
1396-/**
1397- * Callback data for the `LLM_FUNCTION_TOOL_CALL` event type that is triggered when a function tool is called.
1398- */
1399-interface FunctionToolCall {
1400- /**
1401- * Name of the function tool to call
1402- */
1403- name: string;
1404- /**
1405- * JSON object with the parameters to pass to the function tool
1406- */
1407- arguments: string;
1408-}
public/index.html+6 -1
@@ -1873,6 +1873,10 @@
18731873 </div>
18741874 <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact autoSetHeight" name="assistant_impersonation" rows="2" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
18751875 </div>
1876+ <div id="claude_function_prefill_warning">
1877+ <i class="fa-solid fa-circle-info"></i>
1878+ <span>Prefills won't work when function calling is enabled and any tools are registered.</span>
1879+ </div>
18761880 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
18771881 <input id="claude_use_sysprompt" type="checkbox" />
18781882 <span data-i18n="Use system prompt (Claude 2.1+ only)">
@@ -3098,7 +3102,8 @@
30983102 <h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>
30993103 <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.">
31003104 <option data-i18n="prompt_post_processing_none" value="">None</option>
31013105 <option value="claudemerge">ClaudeMerge consecutive roles</option>
3106+ <option value="strict">Strict (user first, alternating roles)</option>
31023107 </select>
31033108 </form>
31043109 <div id="01ai_form" data-source="01ai">
public/jsconfig.json+5 -5
@@ -8,16 +8,16 @@
88 "allowSyntheticDefaultImports": true
99 },
1010 "exclude": [
1111 "**/node_modules/**",
12+ "**/dist/**",
13+ "**/.git/**",
14+ "lib/**",
15+ "**/*.min.js"
1216 ],
1317 "typeAcquisition": {
1418 "include": [
15- "jquery",
1619 "@popperjs/core",
17- "toastr",
1820 "showdown",
19- "dompurify",
20- "moment",
2121 "seedrandom",
2222 "showdown-katex",
2323 "droll",
public/script.js+78 -10
@@ -246,6 +246,7 @@ import { initInputMarkdown } from './scripts/input-md-formatting.js';
246246import { AbortReason } from './scripts/util/AbortReason.js';
247247import { initSystemPrompts } from './scripts/sysprompt.js';
248248import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js';
249+import { ToolManager } from './scripts/tool-calling.js';
249250
250251//exporting functions and vars for mods
251252export {
@@ -470,11 +471,11 @@ export const event_types = {
470471 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
471472 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
472473 OPEN_CHARACTER_LIBRARY: 'open_character_library',
473- LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
474- LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
475474 ONLINE_STATUS_CHANGED: 'online_status_changed',
476475 IMAGE_SWIPED: 'image_swiped',
477476 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
477+ TOOL_CALLS_PERFORMED: 'tool_calls_performed',
478+ TOOL_CALLS_RENDERED: 'tool_calls_rendered',
478479};
479480
480481export const eventSource = new EventEmitter();
@@ -947,6 +948,7 @@ async function firstLoadInit() {
947948 initSystemPrompts();
948949 initExtensions();
949950 initExtensionSlashCommands();
951+ ToolManager.initToolSlashCommands();
950952 await initPresetManager();
951953 await getSystemMessages();
952954 sendSystemMessage(system_message_types.WELCOME);
@@ -2027,7 +2029,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
20272029 // Return the original match if no quotes are found
20282030 return match;
20292031 }
20302032 },
20312033 );
20322034
20332035 // Restore double quotes in tags
@@ -2372,6 +2374,10 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
23722374 newMessage.addClass('smallSysMes');
23732375 }
23742376
2377+ if (Array.isArray(mes?.extra?.tool_invocations)) {
2378+ newMessage.addClass('toolCall');
2379+ }
2380+
23752381 //shows or hides the Prompt display button
23762382 let mesIdToFind = type === 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));
23772383
@@ -2965,6 +2971,7 @@ class StreamingProcessor {
29652971 this.swipes = [];
29662972 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
29672973 this.messageLogprobs = [];
2974+ this.toolCalls = [];
29682975 }
29692976
29702977 #checkDomElements(messageId) {
@@ -2976,6 +2983,13 @@ class StreamingProcessor {
29762983 }
29772984 }
29782985
2986+ #updateMessageBlockVisibility() {
2987+ if (this.messageDom instanceof HTMLElement && Array.isArray(this.toolCalls) && this.toolCalls.length > 0) {
2988+ const shouldHide = ['', '...'].includes(this.result);
2989+ this.messageDom.classList.toggle('displayNone', shouldHide);
2990+ }
2991+ }
2992+
29792993 showMessageButtons(messageId) {
29802994 if (messageId == -1) {
29812995 return;
@@ -3041,6 +3055,7 @@ class StreamingProcessor {
30413055 }
30423056 else {
30433057 this.#checkDomElements(messageId);
3058+ this.#updateMessageBlockVisibility();
30443059 const currentTime = new Date();
30453060 // Don't waste time calculating token count for streaming
30463061 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
@@ -3183,7 +3198,7 @@ class StreamingProcessor {
31833198 }
31843199
31853200 /**
31863201 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>}
31873202 */
31883203 *nullStreamingGeneration() {
31893204 throw new Error('Generation function for streaming is not hooked up');
@@ -3205,12 +3220,13 @@ class StreamingProcessor {
32053220 try {
32063221 const sw = new Stopwatch(1000 / power_user.streaming_fps);
32073222 const timestamps = [];
32083223 for await (const { text, swipes, logprobs, toolCalls } of this.generator()) {
32093224 timestamps.push(Date.now());
32103225 if (this.isStopped) {
32113226 return;
32123227 }
32133228
3229+ this.toolCalls = toolCalls;
32143230 this.result = text;
32153231 this.swipes = Array.from(swipes ?? []);
32163232 if (logprobs) {
@@ -3393,9 +3409,9 @@ function removeLastMessage() {
33933409 * @param {GenerateOptions} options Generation options
33943410 * @param {boolean} dryRun Whether to actually generate a message or just assemble the prompt
33953411 * @returns {Promise<any>} Returns a promise that resolves when the text is done generating.
33963412 * @typedef {{automatic_trigger?: boolean, force_name2?: boolean, quiet_prompt?: string, quietToLoud?: boolean, skipWIAN?: boolean, force_chid?: number, signal?: AbortSignal, quietImage?: string, quietName?: string, depth?: number }} GenerateOptions
33973413 */
33983414export async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, depth = 0 } = {}, dryRun = false) {
33993415 console.log('Generate entered');
34003416 setGenerationProgress(0);
34013417 generation_started = new Date();
@@ -3614,7 +3630,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
36143630 }
36153631
36163632 // Collect messages with usable content
36173633 letconst coreChatcanUseTools = chatToolManager.filterisToolCallingSupported(x => !x.is_system);
3634+ const canPerformToolCalls = !dryRun && ToolManager.canPerformToolCalls(type) && depth < ToolManager.RECURSE_LIMIT;
3635+ let coreChat = chat.filter(x => !x.is_system || (canUseTools && Array.isArray(x.extra?.tool_invocations)));
36183636 if (type === 'swipe') {
36193637 coreChat.pop();
36203638 }
@@ -4449,7 +4467,31 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44494467 getMessage = continue_mag + getMessage;
44504468 }
44514469
44524470 ifconst (isStreamFinished = streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) {;
4471+ const isStreamWithToolCalls = streamingProcessor && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length;
4472+ if (canPerformToolCalls && isStreamFinished && isStreamWithToolCalls) {
4473+ const lastMessage = chat[chat.length - 1];
4474+ const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls);
4475+ const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor?.result);
4476+ hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
4477+ const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);
4478+ if (hasToolCalls) {
4479+ if (!invocationResult.invocations.length && shouldDeleteMessage) {
4480+ ToolManager.showToolCallError(invocationResult.errors);
4481+ unblockGeneration(type);
4482+ generatedPromptCache = '';
4483+ streamingProcessor = null;
4484+ return;
4485+ }
4486+
4487+ streamingProcessor = null;
4488+ depth = depth + 1;
4489+ await ToolManager.saveFunctionToolInvocations(invocationResult.invocations);
4490+ return Generate('normal', { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, depth }, dryRun);
4491+ }
4492+ }
4493+
4494+ if (isStreamFinished) {
44534495 await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage);
44544496 streamingProcessor = null;
44554497 triggerAutoContinue(messageChunk, isImpersonate);
@@ -4523,6 +4565,25 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
45234565 parseAndSaveLogprobs(data, continue_mag);
45244566 }
45254567
4568+ if (canPerformToolCalls) {
4569+ const hasToolCalls = ToolManager.hasToolCalls(data);
4570+ const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage);
4571+ hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
4572+ const invocationResult = await ToolManager.invokeFunctionTools(data);
4573+ if (hasToolCalls) {
4574+ if (!invocationResult.invocations.length && shouldDeleteMessage) {
4575+ ToolManager.showToolCallError(invocationResult.errors);
4576+ unblockGeneration(type);
4577+ generatedPromptCache = '';
4578+ return;
4579+ }
4580+
4581+ depth = depth + 1;
4582+ await ToolManager.saveFunctionToolInvocations(invocationResult.invocations);
4583+ return Generate('normal', { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, depth }, dryRun);
4584+ }
4585+ }
4586+
45264587 if (type !== 'quiet') {
45274588 playMessageSound();
45284589 }
@@ -6737,7 +6798,10 @@ function updateMessage(div) {
67376798 text = getRegexedString(
67386799 text,
67396800 regexPlacement,
6740- { characterOverride: mes.extra?.type === 'narrator' ? undefined : mes.name },
6801+ {
6802+ characterOverride: mes.extra?.type === 'narrator' ? undefined : mes.name,
6803+ isEdit: true,
6804+ },
67416805 );
67426806
67436807
@@ -8174,6 +8238,10 @@ window['SillyTavern'].getContext = function () {
81748238 registerHelper: () => { },
81758239 registerMacro: MacrosParser.registerMacro.bind(MacrosParser),
81768240 unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),
8241+ registerFunctionTool: ToolManager.registerFunctionTool.bind(ToolManager),
8242+ unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
8243+ isToolCallingSupported: ToolManager.isToolCallingSupported.bind(ToolManager),
8244+ canPerformToolCalls: ToolManager.canPerformToolCalls.bind(ToolManager),
81778245 registerDebugFunction: registerDebugFunction,
81788246 /** @deprecated Use renderExtensionTemplateAsync instead. */
81798247 renderExtensionTemplate: renderExtensionTemplate,
public/scripts/extensions/assets/index.js+9 -0
@@ -184,6 +184,7 @@ function downloadAssetsList(url) {
184184 const url = isValidUrl(asset['url']) ? asset['url'] : '';
185185 const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser';
186186 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
187+ const toolTag = assetType === 'extension' && asset['tool'];
187188
188189 const assetBlock = $('<i></i>')
189190 .append(element)
@@ -193,12 +194,20 @@ function downloadAssetsList(url) {
193194 <a class="asset_preview" href="${url}" target="_blank" title="${title}">
194195 <i class="fa-solid fa-sm ${previewIcon}"></i>
195196 </a>
197+ ${toolTag ? '<span class="tag" title="Adds a function tool"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}
196198 </span>
197199 <small class="asset-description">
198200 ${description}
199201 </small>
200202 </div>`);
201203
204+ assetBlock.find('.tag').on('click', function (e) {
205+ const a = document.createElement('a');
206+ a.href = 'https://docs.sillytavern.app/for-contributors/function-calling/';
207+ a.target = '_blank';
208+ a.click();
209+ });
210+
202211 if (assetType === 'character') {
203212 if (asset.highlight) {
204213 assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');
public/scripts/extensions/assets/style.css+9 -0
@@ -164,3 +164,12 @@
164164 gap: 5px;
165165 align-items: center;
166166}
167+
168+.asset-name .tag {
169+ gap: 5px;
170+ align-items: baseline;
171+ font-size: calc(var(--mainFontSize)* 0.8);
172+ cursor: pointer;
173+ opacity: 0.9;
174+ margin-left: 2px;
175+}
public/scripts/extensions/expressions/index.js+1 -51
@@ -9,7 +9,6 @@ import { debounce_timeout } from '../../constants.js';
99import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
1010import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1111import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
12-import { isFunctionCallingSupported } from '../../openai.js';
1312import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
1413import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
1514import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
@@ -21,7 +20,6 @@ const UPDATE_INTERVAL = 2000;
2120const STREAMING_UPDATE_INTERVAL = 10000;
2221const TALKINGCHECK_UPDATE_INTERVAL = 500;
2322const DEFAULT_FALLBACK_EXPRESSION = 'joy';
24-const FUNCTION_NAME = 'set_emotion';
2523const DEFAULT_LLM_PROMPT = 'Ignore previous instructions. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
2624const DEFAULT_EXPRESSIONS = [
2725 'talkinghead',
@@ -1017,10 +1015,6 @@ async function getLlmPrompt(labels) {
10171015 return '';
10181016 }
10191017
1020- if (isFunctionCallingSupported()) {
1021- return '';
1022- }
1023-
10241018 const labelsString = labels.map(x => `"${x}"`).join(', ');
10251019 const prompt = substituteParamsExtended(String(extension_settings.expressions.llmPrompt), { labels: labelsString });
10261020 return prompt;
@@ -1056,41 +1050,6 @@ function parseLlmResponse(emotionResponse, labels) {
10561050 throw new Error('Could not parse emotion response ' + emotionResponse);
10571051}
10581052
1059-/**
1060- * Registers the function tool for the LLM API.
1061- * @param {FunctionToolRegister} args Function tool register arguments.
1062- */
1063-function onFunctionToolRegister(args) {
1064- if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isFunctionCallingSupported()) {
1065- // Only trigger on quiet mode
1066- if (args.type !== 'quiet') {
1067- return;
1068- }
1069-
1070- const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead');
1071- const jsonSchema = {
1072- $schema: 'http://json-schema.org/draft-04/schema#',
1073- type: 'object',
1074- properties: {
1075- emotion: {
1076- type: 'string',
1077- enum: emotions,
1078- description: `One of the following: ${JSON.stringify(emotions)}`,
1079- },
1080- },
1081- required: [
1082- 'emotion',
1083- ],
1084- };
1085- args.registerFunctionTool(
1086- FUNCTION_NAME,
1087- substituteParams('Sets the label that best describes the current emotional state of {{char}}. Only select one of the enumerated values.'),
1088- jsonSchema,
1089- true,
1090- );
1091- }
1092-}
1093-
10941053function onTextGenSettingsReady(args) {
10951054 // Only call if inside an API call
10961055 if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
@@ -1164,18 +1123,9 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
11641123
11651124 const expressionsList = await getExpressionsList();
11661125 const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
1167- let functionResult = null;
11681126 eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
1169- eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);
1170- eventSource.once(event_types.LLM_FUNCTION_TOOL_CALL, (/** @type {FunctionToolCall} */ args) => {
1171- if (args.name !== FUNCTION_NAME) {
1172- return;
1173- }
1174-
1175- functionResult = args?.arguments;
1176- });
11771127 const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
11781128 return parseLlmResponse(functionResult || emotionResponse, expressionsList);
11791129 }
11801130 // Extras
11811131 default: {
public/scripts/extensions/regex/editor.html+1 -1
@@ -117,7 +117,7 @@
117117 <input type="checkbox" name="disabled" />
118118 <span data-i18n="Disabled">Disabled</span>
119119 </label>
120- <label class="checkbox flex-container">
120+ <label class="checkbox flex-container" title="Run the regex script when the message belonging a to specified role(s) is edited.">
121121 <input type="checkbox" name="run_on_edit" />
122122 <span data-i18n="Run On Edit">Run On Edit</span>
123123 </label>
public/scripts/extensions/regex/engine.js+7 -2
@@ -44,9 +44,9 @@ function getScopedRegex() {
4444 * @param {regex_placement} placement The placement of the string
4545 * @param {RegexParams} params The parameters to use for the regex script
4646 * @returns {string} The regexed string
4747 * @typedef {{characterOverride?: string, isMarkdown?: boolean, isPrompt?: boolean, isEdit?: boolean, depth?: number }} RegexParams The parameters to use for the regex script
4848 */
4949function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt, isEdit, depth } = {}) {
5050 // WTF have you passed me?
5151 if (typeof rawString !== 'string') {
5252 console.warn('getRegexedString: rawString is not a string. Returning empty string.');
@@ -68,6 +68,11 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
6868 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
6969 (!script.markdownOnly && !script.promptOnly && !isMarkdown)
7070 ) {
71+ if (isEdit && !script.runOnEdit) {
72+ console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
73+ return;
74+ }
75+
7176 // Check if the depth is within the min/max depth
7277 if (typeof depth === 'number' && depth >= 0) {
7378 if (!isNaN(script.minDepth) && script.minDepth !== null && script.minDepth >= 0 && depth < script.minDepth) {
public/scripts/extensions/stable-diffusion/index.js+67 -4
@@ -32,6 +32,7 @@ import { debounce_timeout } from '../../constants.js';
3232import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
3333import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
3434import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
35+import { ToolManager } from '../../tool-calling.js';
3536export { MODULE_NAME };
3637
3738const MODULE_NAME = 'sd';
@@ -62,9 +63,11 @@ const initiators = {
6263 interactive: 'interactive',
6364 wand: 'wand',
6465 swipe: 'swipe',
66+ tool: 'tool',
6567};
6668
6769const generationMode = {
70+ TOOL: -2,
6871 MESSAGE: -1,
6972 CHARACTER: 0,
7073 USER: 1,
@@ -87,6 +90,7 @@ const multimodalMap = {
8790};
8891
8992const modeLabels = {
93+ [generationMode.TOOL]: 'Function Tool Prompt Description',
9094 [generationMode.MESSAGE]: 'Chat Message Template',
9195 [generationMode.CHARACTER]: 'Character ("Yourself")',
9296 [generationMode.FACE]: 'Portrait ("Your Face")',
@@ -124,8 +128,12 @@ const messageTrigger = {
124128};
125129
126130const promptTemplates = {
127131 // Not really a prompt template, rather an outcome message template and function tool prompt
128132 [generationMode.MESSAGE]: '[{{char}} sends a picture that contains: {{prompt}}].',
133+ [generationMode.TOOL]: [
134+ 'The text prompt used to generate the image.',
135+ 'Must represent an exhaustive description of the desired image that will allow an artist or a photographer to perfectly recreate it.',
136+ ].join(' '),
129137 [generationMode.CHARACTER]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\'',
130138 //face-specific prompt
131139 [generationMode.FACE]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, facial features and expressions, occupation, hair and hair accessories (if any), what they are wearing on their upper body (if anything). Do not describe anything below their neck. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'close up facial portrait,\'',
@@ -226,6 +234,7 @@ const defaultSettings = {
226234 multimodal_captioning: false,
227235 snap: false,
228236 free_extend: false,
237+ function_tool: false,
229238
230239 prompts: promptTemplates,
231240
@@ -291,6 +300,10 @@ const defaultSettings = {
291300const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
292301
293302function processTriggers(chat, _, abort) {
303+ if (extension_settings.sd.function_tool && ToolManager.isToolCallingSupported()) {
304+ return;
305+ }
306+
294307 if (!extension_settings.sd.interactive_mode) {
295308 return;
296309 }
@@ -447,6 +460,7 @@ async function loadSettings() {
447460 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
448461 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
449462 $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);
463+ $('#sd_function_tool').prop('checked', extension_settings.sd.function_tool);
450464
451465 for (const style of extension_settings.sd.styles) {
452466 const option = document.createElement('option');
@@ -461,6 +475,7 @@ async function loadSettings() {
461475
462476 toggleSourceControls();
463477 addPromptTemplates();
478+ registerFunctionTool();
464479
465480 await loadSettingOptions();
466481}
@@ -524,6 +539,9 @@ function addPromptTemplates() {
524539 .on('click', () => {
525540 textarea.val(promptTemplates[name]);
526541 extension_settings.sd.prompts[name] = promptTemplates[name];
542+ if (String(name) === String(generationMode.TOOL)) {
543+ registerFunctionTool();
544+ }
527545 saveSettingsDebounced();
528546 });
529547 const container = $('<div></div>')
@@ -910,6 +928,12 @@ async function onSourceChange() {
910928 await loadSettingOptions();
911929}
912930
931+function onFunctionToolInput() {
932+ extension_settings.sd.function_tool = !!$(this).prop('checked');
933+ saveSettingsDebounced();
934+ registerFunctionTool();
935+}
936+
913937async function onOpenAiStyleSelect() {
914938 extension_settings.sd.openai_style = String($('#sd_openai_style').find(':selected').val());
915939 saveSettingsDebounced();
@@ -2290,9 +2314,9 @@ async function generatePicture(initiator, args, trigger, message, callback) {
22902314 eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });
22912315
22922316 if (typeof callbackOriginal === 'function') {
22932317 await callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix, initiator);
22942318 } else {
22952319 await sendMessage(prompt, imagePath, generationType, negativePromptPrefix, initiator);
22962320 }
22972321 };
22982322 }
@@ -2621,7 +2645,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
26212645
26222646 const filename = `${characterName}_${humanizedDateTime()}`;
26232647 const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format);
2624- callback ? callback(prompt, base64Image, generationType, additionalNegativePrefix, initiator) : sendMessage(prompt, base64Image, generationType, additionalNegativePrefix, initiator);
2648+ callback
2649+ ? await callback(prompt, base64Image, generationType, additionalNegativePrefix, initiator)
2650+ : await sendMessage(prompt, base64Image, generationType, additionalNegativePrefix, initiator);
26252651 return base64Image;
26262652}
26272653
@@ -3822,6 +3848,42 @@ function applyCommandArguments(args) {
38223848 return currentSettings;
38233849}
38243850
3851+function registerFunctionTool() {
3852+ if (!extension_settings.sd.function_tool) {
3853+ return ToolManager.unregisterFunctionTool('GenerateImage');
3854+ }
3855+
3856+ ToolManager.registerFunctionTool({
3857+ name: 'GenerateImage',
3858+ displayName: 'Generate Image',
3859+ description: [
3860+ 'Generate an image from a given text prompt.',
3861+ 'Use when a user asks for an image, a selfie, to picture a scene, etc.',
3862+ ].join(' '),
3863+ parameters: Object.freeze({
3864+ $schema: 'http://json-schema.org/draft-04/schema#',
3865+ type: 'object',
3866+ properties: {
3867+ prompt: {
3868+ type: 'string',
3869+ description: extension_settings.sd.prompts[generationMode.TOOL] || promptTemplates[generationMode.TOOL],
3870+ },
3871+ },
3872+ required: [
3873+ 'prompt',
3874+ ],
3875+ }),
3876+ action: async (args) => {
3877+ if (!isValidState()) throw new Error('Image generation is not configured.');
3878+ if (!args) throw new Error('Missing arguments');
3879+ if (!args.prompt) throw new Error('Missing prompt');
3880+ const url = await generatePicture(initiators.tool, {}, args.prompt);
3881+ return encodeURI(url);
3882+ },
3883+ formatMessage: () => 'Generating an image...',
3884+ });
3885+}
3886+
38253887jQuery(async () => {
38263888 await addSDGenButtons();
38273889
@@ -4175,6 +4237,7 @@ jQuery(async () => {
41754237 $('#sd_stability_key').on('click', onStabilityKeyClick);
41764238 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
41774239 $('#sd_huggingface_model_id').on('input', onHFModelInput);
4240+ $('#sd_function_tool').on('input', onFunctionToolInput);
41784241
41794242 if (!CSS.supports('field-sizing', 'content')) {
41804243 $('.sd_settings .inline-drawer-toggle').on('click', function () {
public/scripts/extensions/stable-diffusion/settings.html+5 -1
@@ -14,9 +14,13 @@
1414 <input id="sd_refine_mode" type="checkbox" />
1515 <span data-i18n="sd_refine_mode_txt">Edit prompts before generation</span>
1616 </label>
17+ <label for="sd_function_tool" class="checkbox_label" data-i18n="[title]sd_function_tool" title="Use the function tool to automatically detect intents to generate images.">
18+ <input id="sd_function_tool" type="checkbox" />
19+ <span data-i18n="sd_function_tool_txt">Use function tool</span>
20+ </label>
1721 <label for="sd_interactive_mode" class="checkbox_label" data-i18n="[title]sd_interactive_mode" title="Automatically generate images when sending messages like 'send me a picture of cat'.">
1822 <input id="sd_interactive_mode" type="checkbox" />
1923 <span data-i18n="sd_interactive_mode_txt">InteractiveUse interactive mode</span>
2024 </label>
2125 <label for="sd_multimodal_captioning" class="checkbox_label" data-i18n="[title]sd_multimodal_captioning" title="Use multimodal captioning to generate prompts for user and character portraits based on their avatars.">
2226 <input id="sd_multimodal_captioning" type="checkbox" />
public/scripts/group-chats.js+1 -1
@@ -70,12 +70,12 @@ import {
7070 animation_duration,
7171 depth_prompt_role_default,
7272 shouldAutoContinue,
73- this_chid,
7473} from '../script.js';
7574import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
7675import { FILTER_TYPES, FilterHelper } from './filters.js';
7776import { isExternalMediaAllowed } from './chats.js';
7877import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
78+import { t } from './i18n.js';
7979
8080export {
8181 selected_group,
public/scripts/instruct-mode.js+0 -2
@@ -554,11 +554,9 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
554554 * @param {string} name Preset name.
555555 */
556556function selectMatchingContextTemplate(name) {
557- let foundMatch = false;
558557 for (const context_preset of context_presets) {
559558 // If context template matches the instruct preset
560559 if (context_preset.name === name) {
561- foundMatch = true;
562560 selectContextPreset(context_preset.name, { isAuto: true });
563561 break;
564562 }
public/scripts/kai-settings.js+1 -1
@@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
188188 if (data?.token) {
189189 text += data.token;
190190 }
191191 yield { text, swipes: [], toolCalls: [] };
192192 }
193193 };
194194}
public/scripts/nai-settings.js+1 -1
@@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
746746 text += data.token;
747747 }
748748
749749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [] };
750750 }
751751 };
752752}
public/scripts/openai.js+82 -149
@@ -70,6 +70,7 @@ import { renderTemplateAsync } from './templates.js';
7070import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
7171import { Popup, POPUP_RESULT } from './popup.js';
7272import { t } from './i18n.js';
73+import { ToolManager } from './tool-calling.js';
7374
7475export {
7576 openai_messages_count,
@@ -198,7 +199,10 @@ const continue_postfix_types = {
198199
199200const custom_prompt_post_processing_types = {
200201 NONE: '',
202+ /** @deprecated Use MERGE instead. */
201203 CLAUDE: 'claude',
204+ MERGE: 'merge',
205+ STRICT: 'strict',
202206};
203207
204208const sensitiveFields = [
@@ -453,7 +457,8 @@ function setOpenAIMessages(chat) {
453457 if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`;
454458 const name = chat[j]['name'];
455459 const image = chat[j]?.extra?.image;
456- messages[i] = { 'role': role, 'content': content, name: name, 'image': image };
460+ const invocations = chat[j]?.extra?.tool_invocations;
461+ messages[i] = { 'role': role, 'content': content, name: name, 'image': image, 'invocations': invocations };
457462 j++;
458463 }
459464
@@ -701,6 +706,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
701706 }
702707
703708 const imageInlining = isImageInliningSupported();
709+ const canUseTools = ToolManager.isToolCallingSupported();
704710
705711 // Insert chat messages as long as there is budget available
706712 const chatPool = [...messages].reverse();
@@ -722,6 +728,24 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
722728 await chatMessage.addImage(chatPrompt.image);
723729 }
724730
731+ if (canUseTools && Array.isArray(chatPrompt.invocations)) {
732+ /** @type {import('./tool-calling.js').ToolInvocation[]} */
733+ const invocations = chatPrompt.invocations;
734+ const toolCallMessage = new Message(chatMessage.role, undefined, 'toolCall-' + chatMessage.identifier);
735+ const toolResultMessages = invocations.slice().reverse().map((invocation) => new Message('tool', invocation.result || '[No content]', invocation.id));
736+ toolCallMessage.setToolCalls(invocations);
737+ if (chatCompletion.canAffordAll([toolCallMessage, ...toolResultMessages])) {
738+ for (const resultMessage of toolResultMessages) {
739+ chatCompletion.insertAtStart(resultMessage, 'chatHistory');
740+ }
741+ chatCompletion.insertAtStart(toolCallMessage, 'chatHistory');
742+ } else {
743+ break;
744+ }
745+
746+ continue;
747+ }
748+
725749 if (chatCompletion.canAfford(chatMessage)) {
726750 if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {
727751 // in case we are using continue_prefill and the latest message is an assistant message, we want to prepend the users assistant prefill on the message
@@ -1262,7 +1286,7 @@ export async function prepareOpenAIMessages({
12621286 const eventData = { chat, dryRun };
12631287 await eventSource.emit(event_types.CHAT_COMPLETION_PROMPT_READY, eventData);
12641288
12651289 openai_messages_count = chat.filter(x => !x?.tool_calls && (x?.role === 'user' || x?.role === 'assistant'))?.length || 0;
12661290
12671291 return [chat, promptManager.tokenHandler.counts];
12681292}
@@ -1687,7 +1711,6 @@ async function sendOpenAIRequest(type, messages, signal) {
16871711 messages = messages.filter(msg => msg && typeof msg === 'object');
16881712
16891713 let logit_bias = {};
1690- const messageId = getNextMessageId(type);
16911714 const isClaude = oai_settings.chat_completion_source == chat_completion_sources.CLAUDE;
16921715 const isOpenRouter = oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER;
16931716 const isScale = oai_settings.chat_completion_source == chat_completion_sources.SCALE;
@@ -1860,8 +1883,8 @@ async function sendOpenAIRequest(type, messages, signal) {
18601883 generate_data['seed'] = oai_settings.seed;
18611884 }
18621885
18631886 if (isFunctionCallingSupported()!canMultiSwipe && !streamToolManager.canPerformToolCalls(type)) {
18641887 await registerFunctionToolsToolManager.registerFunctionToolsOpenAI(type, generate_data);
18651888 }
18661889
18671890 if (isOAI && oai_settings.openai_model.startsWith('o1-')) {
@@ -1908,6 +1931,7 @@ async function sendOpenAIRequest(type, messages, signal) {
19081931 return async function* streamData() {
19091932 let text = '';
19101933 const swipes = [];
1934+ const toolCalls = [];
19111935 while (true) {
19121936 const { done, value } = await reader.read();
19131937 if (done) return;
@@ -1923,7 +1947,9 @@ async function sendOpenAIRequest(type, messages, signal) {
19231947 text += getStreamingReply(parsed);
19241948 }
19251949
1926- yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed) };
1950+ ToolManager.parseToolCalls(toolCalls, parsed);
1951+
1952+ yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls };
19271953 }
19281954 };
19291955 }
@@ -1945,147 +1971,10 @@ async function sendOpenAIRequest(type, messages, signal) {
19451971 delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null));
19461972 }
19471973
1948- if (isFunctionCallingSupported()) {
1949- await checkFunctionToolCalls(data);
1950- }
1951-
19521974 return data;
19531975 }
19541976}
19551977
1956-/**
1957- * Register function tools for the next chat completion request.
1958- * @param {string} type Generation type
1959- * @param {object} data Generation data
1960- */
1961-async function registerFunctionTools(type, data) {
1962- let toolChoice = 'auto';
1963- const tools = [];
1964-
1965- /**
1966- * @type {registerFunctionTool}
1967- */
1968- const registerFunctionTool = (name, description, parameters, required) => {
1969- tools.push({
1970- type: 'function',
1971- function: {
1972- name,
1973- description,
1974- parameters,
1975- },
1976- });
1977-
1978- if (required) {
1979- toolChoice = 'required';
1980- }
1981- };
1982-
1983- /**
1984- * @type {FunctionToolRegister}
1985- */
1986- const args = {
1987- type,
1988- data,
1989- registerFunctionTool,
1990- };
1991-
1992- await eventSource.emit(event_types.LLM_FUNCTION_TOOL_REGISTER, args);
1993-
1994- if (tools.length) {
1995- console.log('Registered function tools:', tools);
1996-
1997- data['tools'] = tools;
1998- data['tool_choice'] = toolChoice;
1999- }
2000-}
2001-
2002-async function checkFunctionToolCalls(data) {
2003- const oaiCompat = [
2004- chat_completion_sources.OPENAI,
2005- chat_completion_sources.CUSTOM,
2006- chat_completion_sources.MISTRALAI,
2007- chat_completion_sources.OPENROUTER,
2008- chat_completion_sources.GROQ,
2009- ];
2010- if (oaiCompat.includes(oai_settings.chat_completion_source)) {
2011- if (!Array.isArray(data?.choices)) {
2012- return;
2013- }
2014-
2015- // Find a choice with 0-index
2016- const choice = data.choices.find(choice => choice.index === 0);
2017-
2018- if (!choice) {
2019- return;
2020- }
2021-
2022- const toolCalls = choice.message.tool_calls;
2023-
2024- if (!Array.isArray(toolCalls)) {
2025- return;
2026- }
2027-
2028- for (const toolCall of toolCalls) {
2029- if (typeof toolCall.function !== 'object') {
2030- continue;
2031- }
2032-
2033- /** @type {FunctionToolCall} */
2034- const args = toolCall.function;
2035- console.log('Function tool call:', toolCall);
2036- await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
2037- }
2038- }
2039-
2040- if ([chat_completion_sources.CLAUDE].includes(oai_settings.chat_completion_source)) {
2041- if (!Array.isArray(data?.content)) {
2042- return;
2043- }
2044-
2045- for (const content of data.content) {
2046- if (content.type === 'tool_use') {
2047- /** @type {FunctionToolCall} */
2048- const args = { name: content.name, arguments: JSON.stringify(content.input) };
2049- await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
2050- }
2051- }
2052- }
2053-
2054- if ([chat_completion_sources.COHERE].includes(oai_settings.chat_completion_source)) {
2055- if (!Array.isArray(data?.tool_calls)) {
2056- return;
2057- }
2058-
2059- for (const toolCall of data.tool_calls) {
2060- /** @type {FunctionToolCall} */
2061- const args = { name: toolCall.name, arguments: JSON.stringify(toolCall.parameters) };
2062- console.log('Function tool call:', toolCall);
2063- await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
2064- }
2065- }
2066-}
2067-
2068-export function isFunctionCallingSupported() {
2069- if (main_api !== 'openai') {
2070- return false;
2071- }
2072-
2073- if (!oai_settings.function_calling) {
2074- return false;
2075- }
2076-
2077- const supportedSources = [
2078- chat_completion_sources.OPENAI,
2079- chat_completion_sources.COHERE,
2080- chat_completion_sources.CUSTOM,
2081- chat_completion_sources.MISTRALAI,
2082- chat_completion_sources.CLAUDE,
2083- chat_completion_sources.OPENROUTER,
2084- chat_completion_sources.GROQ,
2085- ];
2086- return supportedSources.includes(oai_settings.chat_completion_source);
2087-}
2088-
20891978function getStreamingReply(data) {
20901979 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
20911980 return data?.delta?.text || '';
@@ -2323,6 +2212,8 @@ class Message {
23232212 content;
23242213 /** @type {string} */
23252214 name;
2215+ /** @type {object} */
2216+ tool_call = null;
23262217
23272218 /**
23282219 * @constructor
@@ -2347,6 +2238,22 @@ class Message {
23472238 }
23482239 }
23492240
2241+ /**
2242+ * Reconstruct the message from a tool invocation.
2243+ * @param {import('./tool-calling.js').ToolInvocation[]} invocations
2244+ */
2245+ setToolCalls(invocations) {
2246+ this.tool_calls = invocations.map(i => ({
2247+ id: i.id,
2248+ type: 'function',
2249+ function: {
2250+ arguments: i.parameters,
2251+ name: i.name,
2252+ },
2253+ }));
2254+ this.tokens = tokenHandler.count({ role: this.role, tool_calls: JSON.stringify(this.tool_calls) });
2255+ }
2256+
23502257 setName(name) {
23512258 this.name = name;
23522259 this.tokens = tokenHandler.count({ role: this.role, content: this.content, name: this.name });
@@ -2483,13 +2390,20 @@ class MessageCollection {
24832390 }
24842391
24852392 /**
24862393 * Get chat in the format of {role, name, content, tool_calls}.
24872394 * @returns {Array} Array of objects with role, name, and content properties.
24882395 */
24892396 getChat() {
24902397 return this.collection.reduce((acc, message) => {
2491- const name = message.name;
2398+ if (message.content || message.tool_calls) {
2492- if (message.content) acc.push({ role: message.role, ...(name && { name }), content: message.content });
2399+ acc.push({
2400+ role: message.role,
2401+ content: message.content,
2402+ ...(message.name && { name: message.name }),
2403+ ...(message.tool_calls && { tool_calls: message.tool_calls }),
2404+ ...(message.role === 'tool' && { tool_call_id: message.identifier }),
2405+ });
2406+ }
24932407 return acc;
24942408 }, []);
24952409 }
@@ -2694,7 +2608,7 @@ export class ChatCompletion {
26942608 this.checkTokenBudget(message, message.identifier);
26952609
26962610 const index = this.findMessageIndex(identifier);
26972611 if (message.content || message.tool_calls) {
26982612 if ('start' === position) this.messages.collection[index].collection.unshift(message);
26992613 else if ('end' === position) this.messages.collection[index].collection.push(message);
27002614 else if (typeof position === 'number') this.messages.collection[index].collection.splice(position, 0, message);
@@ -2735,6 +2649,15 @@ export class ChatCompletion {
27352649 }
27362650
27372651 /**
2652+ * Checks if the token budget can afford the tokens of all the specified messages.
2653+ * @param {Message[]} messages - The messages to check for affordability.
2654+ * @returns {boolean} True if the budget can afford all the messages, false otherwise.
2655+ */
2656+ canAffordAll(messages) {
2657+ return 0 <= this.tokenBudget - messages.reduce((total, message) => total + message.getTokens(), 0);
2658+ }
2659+
2660+ /**
27382661 * Checks if a message with the specified identifier exists in the collection.
27392662 *
27402663 * @param {string} identifier - The identifier to check for existence.
@@ -2763,8 +2686,14 @@ export class ChatCompletion {
27632686 for (let item of this.messages.collection) {
27642687 if (item instanceof MessageCollection) {
27652688 chat.push(...item.getChat());
27662689 } else if (item instanceof Message && (item.content || item.tool_calls)) {
2767- const message = { role: item.role, content: item.content, ...(item.name ? { name: item.name } : {}) };
2690+ const message = {
2691+ role: item.role,
2692+ content: item.content,
2693+ ...(item.name ? { name: item.name } : {}),
2694+ ...(item.tool_calls ? { tool_calls: item.tool_calls } : {}),
2695+ ...(item.role === 'tool' ? { tool_call_id: item.identifier } : {}),
2696+ };
27682697 chat.push(message);
27692698 } else {
27702699 this.log(`Skipping invalid or empty message in collection: ${JSON.stringify(item)}`);
@@ -3118,6 +3047,10 @@ function loadOpenAISettings(data, settings) {
31183047 setNamesBehaviorControls();
31193048 setContinuePostfixControls();
31203049
3050+ if (oai_settings.custom_prompt_post_processing === custom_prompt_post_processing_types.CLAUDE) {
3051+ oai_settings.custom_prompt_post_processing = custom_prompt_post_processing_types.MERGE;
3052+ }
3053+
31213054 $('#chat_completion_source').val(oai_settings.chat_completion_source).trigger('change');
31223055 $('#oai_max_context_unlocked').prop('checked', oai_settings.max_context_unlocked);
31233056 $('#custom_prompt_post_processing').val(oai_settings.custom_prompt_post_processing);
public/scripts/slash-commands/SlashCommandReturnHelper.js+1 -1
@@ -58,7 +58,7 @@ export const slashCommandReturnHelper = {
5858 case 'toast-html': {
5959 const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue);
6060
6161 if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT, '', { allowVerticalScrolling: true, wide: true });
6262 if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml);
6363 if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml });
6464
public/scripts/textgen-settings.js+2 -1
@@ -916,6 +916,7 @@ async function generateTextGenWithStreaming(generate_data, signal) {
916916 /** @type {import('./logprobs.js').TokenLogprobs | null} */
917917 let logprobs = null;
918918 const swipes = [];
919+ const toolCalls = [];
919920 while (true) {
920921 const { done, value } = await reader.read();
921922 if (done) return;
@@ -934,7 +935,7 @@ async function generateTextGenWithStreaming(generate_data, signal) {
934935 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
935936 }
936937
937938 yield { text, swipes, logprobs, toolCalls };
938939 }
939940 };
940941}
public/scripts/tool-calling.js+891 -0
@@ -0,0 +1,891 @@
1+import { addOneMessage, chat, event_types, eventSource, main_api, saveChatConditional, system_avatar, systemUserName } from '../script.js';
2+import { chat_completion_sources, oai_settings } from './openai.js';
3+import { Popup } from './popup.js';
4+import { SlashCommand } from './slash-commands/SlashCommand.js';
5+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
6+import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
7+import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
8+import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
9+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
10+import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
11+
12+/**
13+ * @typedef {object} ToolInvocation
14+ * @property {string} id - A unique identifier for the tool invocation.
15+ * @property {string} displayName - The display name of the tool.
16+ * @property {string} name - The name of the tool.
17+ * @property {string} parameters - The parameters for the tool invocation.
18+ * @property {string} result - The result of the tool invocation.
19+ */
20+
21+/**
22+ * @typedef {object} ToolInvocationResult
23+ * @property {ToolInvocation[]} invocations Successful tool invocations
24+ * @property {Error[]} errors Errors that occurred during tool invocation
25+ */
26+
27+/**
28+ * @typedef {object} ToolRegistration
29+ * @property {string} name - The name of the tool.
30+ * @property {string} displayName - The display name of the tool.
31+ * @property {string} description - A description of the tool.
32+ * @property {object} parameters - The parameters for the tool.
33+ * @property {function} action - The action to perform when the tool is invoked.
34+ * @property {function} formatMessage - A function to format the tool call message.
35+ */
36+
37+/**
38+ * @typedef {object} ToolDefinitionOpenAI
39+ * @property {string} type - The type of the tool.
40+ * @property {object} function - The function definition.
41+ * @property {string} function.name - The name of the function.
42+ * @property {string} function.description - The description of the function.
43+ * @property {object} function.parameters - The parameters of the function.
44+ * @property {function} toString - A function to convert the tool to a string.
45+ */
46+
47+/**
48+ * Assigns nested variables to a scope.
49+ * @param {import('./slash-commands/SlashCommandScope.js').SlashCommandScope} scope The scope to assign variables to.
50+ * @param {object} arg Object to assign variables from.
51+ * @param {string} prefix Prefix for the variable names.
52+ */
53+function assignNestedVariables(scope, arg, prefix) {
54+ Object.entries(arg).forEach(([key, value]) => {
55+ const newPrefix = `${prefix}.${key}`;
56+ if (typeof value === 'object' && value !== null) {
57+ assignNestedVariables(scope, value, newPrefix);
58+ } else {
59+ scope.letVariable(newPrefix, value);
60+ }
61+ });
62+}
63+
64+/**
65+ * Checks if a string is a valid JSON string.
66+ * @param {string} str The string to check
67+ * @returns {boolean} If the string is a valid JSON string
68+ */
69+function isJson(str) {
70+ try {
71+ JSON.parse(str);
72+ return true;
73+ } catch {
74+ return false;
75+ }
76+}
77+
78+/**
79+ * Tries to parse a string as JSON, returning the original string if parsing fails.
80+ * @param {string} str The string to try to parse
81+ * @returns {object|string} Parsed JSON or the original string
82+ */
83+function tryParse(str) {
84+ try {
85+ return JSON.parse(str);
86+ } catch {
87+ return str;
88+ }
89+}
90+
91+/**
92+ * Stringifies an object if it is not already a string.
93+ * @param {any} obj The object to stringify
94+ * @returns {string} A JSON string representation of the object.
95+ */
96+function stringify(obj) {
97+ return typeof obj === 'string' ? obj : JSON.stringify(obj);
98+}
99+
100+/**
101+ * A class that represents a tool definition.
102+ */
103+class ToolDefinition {
104+ /**
105+ * A unique name for the tool.
106+ * @type {string}
107+ */
108+ #name;
109+
110+ /**
111+ * A user-friendly display name for the tool.
112+ * @type {string}
113+ */
114+ #displayName;
115+
116+ /**
117+ * A description of what the tool does.
118+ * @type {string}
119+ */
120+ #description;
121+
122+ /**
123+ * A JSON schema for the parameters that the tool accepts.
124+ * @type {object}
125+ */
126+ #parameters;
127+
128+ /**
129+ * A function that will be called when the tool is executed.
130+ * @type {function}
131+ */
132+ #action;
133+
134+ /**
135+ * A function that will be called to format the tool call toast.
136+ * @type {function}
137+ */
138+ #formatMessage;
139+
140+ /**
141+ * Creates a new ToolDefinition.
142+ * @param {string} name A unique name for the tool.
143+ * @param {string} displayName A user-friendly display name for the tool.
144+ * @param {string} description A description of what the tool does.
145+ * @param {object} parameters A JSON schema for the parameters that the tool accepts.
146+ * @param {function} action A function that will be called when the tool is executed.
147+ * @param {function} formatMessage A function that will be called to format the tool call toast.
148+ */
149+ constructor(name, displayName, description, parameters, action, formatMessage) {
150+ this.#name = name;
151+ this.#displayName = displayName;
152+ this.#description = description;
153+ this.#parameters = parameters;
154+ this.#action = action;
155+ this.#formatMessage = formatMessage;
156+ }
157+
158+ /**
159+ * Converts the ToolDefinition to an OpenAI API representation
160+ * @returns {ToolDefinitionOpenAI} OpenAI API representation of the tool.
161+ */
162+ toFunctionOpenAI() {
163+ return {
164+ type: 'function',
165+ function: {
166+ name: this.#name,
167+ description: this.#description,
168+ parameters: this.#parameters,
169+ },
170+ toString: function () {
171+ return `<div><b>${this.function.name}</b></div><div><small>${this.function.description}</small></div><pre class="justifyLeft wordBreakAll"><code class="flex padding5">${JSON.stringify(this.function.parameters, null, 2)}</code></pre><hr>`;
172+ },
173+ };
174+ }
175+
176+ /**
177+ * Invokes the tool with the given parameters.
178+ * @param {object} parameters The parameters to pass to the tool.
179+ * @returns {Promise<any>} The result of the tool's action function.
180+ */
181+ async invoke(parameters) {
182+ return await this.#action(parameters);
183+ }
184+
185+ /**
186+ * Formats a message with the tool invocation.
187+ * @param {object} parameters The parameters to pass to the tool.
188+ * @returns {string} The formatted message.
189+ */
190+ formatMessage(parameters) {
191+ return typeof this.#formatMessage === 'function'
192+ ? this.#formatMessage(parameters)
193+ : `Invoking tool: ${this.#displayName || this.#name}`;
194+ }
195+
196+ get displayName() {
197+ return this.#displayName;
198+ }
199+}
200+
201+/**
202+ * A class that manages the registration and invocation of tools.
203+ */
204+export class ToolManager {
205+ /**
206+ * A map of tool names to tool definitions.
207+ * @type {Map<string, ToolDefinition>}
208+ */
209+ static #tools = new Map();
210+
211+ static #INPUT_DELTA_KEY = '__input_json_delta';
212+
213+ /**
214+ * The maximum number of times to recurse when parsing tool calls.
215+ * @type {number}
216+ */
217+ static RECURSE_LIMIT = 5;
218+
219+ /**
220+ * Returns an Array of all tools that have been registered.
221+ * @type {ToolDefinition[]}
222+ */
223+ static get tools() {
224+ return Array.from(this.#tools.values());
225+ }
226+
227+ /**
228+ * Registers a new tool with the tool registry.
229+ * @param {ToolRegistration} tool The tool to register.
230+ */
231+ static registerFunctionTool({ name, displayName, description, parameters, action, formatMessage }) {
232+ // Convert WIP arguments
233+ if (typeof arguments[0] !== 'object') {
234+ [name, description, parameters, action] = arguments;
235+ }
236+
237+ if (this.#tools.has(name)) {
238+ console.warn(`A tool with the name "${name}" has already been registered. The definition will be overwritten.`);
239+ }
240+
241+ const definition = new ToolDefinition(name, displayName, description, parameters, action, formatMessage);
242+ this.#tools.set(name, definition);
243+ console.log('[ToolManager] Registered function tool:', definition);
244+ }
245+
246+ /**
247+ * Removes a tool from the tool registry.
248+ * @param {string} name The name of the tool to unregister.
249+ */
250+ static unregisterFunctionTool(name) {
251+ if (!this.#tools.has(name)) {
252+ return;
253+ }
254+
255+ this.#tools.delete(name);
256+ console.log(`[ToolManager] Unregistered function tool: ${name}`);
257+ }
258+
259+ /**
260+ * Invokes a tool by name. Returns the result of the tool's action function.
261+ * @param {string} name The name of the tool to invoke.
262+ * @param {object} parameters Function parameters. For example, if the tool requires a "name" parameter, you would pass {name: "value"}.
263+ * @returns {Promise<string|Error>} The result of the tool's action function. If an error occurs, null is returned. Non-string results are JSON-stringified.
264+ */
265+ static async invokeFunctionTool(name, parameters) {
266+ try {
267+ if (!this.#tools.has(name)) {
268+ throw new Error(`No tool with the name "${name}" has been registered.`);
269+ }
270+
271+ const invokeParameters = typeof parameters === 'string' ? JSON.parse(parameters) : parameters;
272+ const tool = this.#tools.get(name);
273+ const result = await tool.invoke(invokeParameters);
274+ return typeof result === 'string' ? result : JSON.stringify(result);
275+ } catch (error) {
276+ console.error(`An error occurred while invoking the tool "${name}":`, error);
277+
278+ if (error instanceof Error) {
279+ error.cause = name;
280+ return error;
281+ }
282+
283+ return new Error('Unknown error occurred while invoking the tool.', { cause: name });
284+ }
285+ }
286+
287+ /**
288+ * Formats a message for a tool call by name.
289+ * @param {string} name The name of the tool to format the message for.
290+ * @param {object} parameters Function tool call parameters.
291+ * @returns {string} The formatted message for the tool call.
292+ */
293+ static formatToolCallMessage(name, parameters) {
294+ if (!this.#tools.has(name)) {
295+ return `Invoked unknown tool: ${name}`;
296+ }
297+
298+ try {
299+ const tool = this.#tools.get(name);
300+ const formatParameters = typeof parameters === 'string' ? JSON.parse(parameters) : parameters;
301+ return tool.formatMessage(formatParameters);
302+ } catch (error) {
303+ console.error(`An error occurred while formatting the tool call message for "${name}":`, error);
304+ return `Invoking tool: ${name}`;
305+ }
306+ }
307+
308+ /**
309+ * Gets the display name of a tool by name.
310+ * @param {string} name
311+ * @returns {string} The display name of the tool.
312+ */
313+ static getDisplayName(name) {
314+ if (!this.#tools.has(name)) {
315+ return name;
316+ }
317+
318+ const tool = this.#tools.get(name);
319+ return tool.displayName || name;
320+ }
321+
322+ /**
323+ * Register function tools for the next chat completion request.
324+ * @param {object} data Generation data
325+ */
326+ static async registerFunctionToolsOpenAI(data) {
327+ const tools = [];
328+
329+ for (const tool of ToolManager.tools) {
330+ tools.push(tool.toFunctionOpenAI());
331+ }
332+
333+ if (tools.length) {
334+ console.log('Registered function tools:', tools);
335+
336+ data['tools'] = tools;
337+ data['tool_choice'] = 'auto';
338+ }
339+ }
340+
341+ /**
342+ * Utility function to parse tool calls from a parsed response.
343+ * @param {any[]} toolCalls The tool calls to update.
344+ * @param {any} parsed The parsed response from the OpenAI API.
345+ * @returns {void}
346+ */
347+ static parseToolCalls(toolCalls, parsed) {
348+ if (Array.isArray(parsed?.choices)) {
349+ for (const choice of parsed.choices) {
350+ const choiceIndex = (typeof choice.index === 'number') ? choice.index : null;
351+ const choiceDelta = choice.delta;
352+
353+ if (choiceIndex === null || !choiceDelta) {
354+ continue;
355+ }
356+
357+ const toolCallDeltas = choiceDelta?.tool_calls;
358+
359+ if (!Array.isArray(toolCallDeltas)) {
360+ continue;
361+ }
362+
363+ if (!Array.isArray(toolCalls[choiceIndex])) {
364+ toolCalls[choiceIndex] = [];
365+ }
366+
367+ for (const toolCallDelta of toolCallDeltas) {
368+ const toolCallIndex = (typeof toolCallDelta?.index === 'number') ? toolCallDelta.index : toolCallDeltas.indexOf(toolCallDelta);
369+
370+ if (isNaN(toolCallIndex) || toolCallIndex < 0) {
371+ continue;
372+ }
373+
374+ if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
375+ toolCalls[choiceIndex][toolCallIndex] = {};
376+ }
377+
378+ const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
379+
380+ ToolManager.#applyToolCallDelta(targetToolCall, toolCallDelta);
381+ }
382+ }
383+ }
384+ if (typeof parsed?.content_block === 'object') {
385+ const choiceIndex = 0;
386+ const toolCallIndex = parsed?.index ?? 0;
387+
388+ if (parsed?.content_block?.type === 'tool_use') {
389+ if (!Array.isArray(toolCalls[choiceIndex])) {
390+ toolCalls[choiceIndex] = [];
391+ }
392+ if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
393+ toolCalls[choiceIndex][toolCallIndex] = {};
394+ }
395+ const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
396+ ToolManager.#applyToolCallDelta(targetToolCall, parsed.content_block);
397+ }
398+ }
399+ if (typeof parsed?.delta === 'object') {
400+ const choiceIndex = 0;
401+ const toolCallIndex = parsed?.index ?? 0;
402+ const targetToolCall = toolCalls[choiceIndex]?.[toolCallIndex];
403+ if (targetToolCall) {
404+ if (parsed?.delta?.type === 'input_json_delta') {
405+ const jsonDelta = parsed?.delta?.partial_json;
406+ if (!targetToolCall[this.#INPUT_DELTA_KEY]) {
407+ targetToolCall[this.#INPUT_DELTA_KEY] = '';
408+ }
409+ targetToolCall[this.#INPUT_DELTA_KEY] += jsonDelta;
410+ }
411+ }
412+ }
413+ if (parsed?.type === 'content_block_stop') {
414+ const choiceIndex = 0;
415+ const toolCallIndex = parsed?.index ?? 0;
416+ const targetToolCall = toolCalls[choiceIndex]?.[toolCallIndex];
417+ if (targetToolCall) {
418+ const jsonDeltaString = targetToolCall[this.#INPUT_DELTA_KEY];
419+ if (jsonDeltaString) {
420+ try {
421+ const jsonDelta = { input: JSON.parse(jsonDeltaString) };
422+ delete targetToolCall[this.#INPUT_DELTA_KEY];
423+ ToolManager.#applyToolCallDelta(targetToolCall, jsonDelta);
424+ } catch (error) {
425+ console.warn('Failed to apply input JSON delta:', error);
426+ }
427+ }
428+ }
429+ }
430+ }
431+
432+ /**
433+ * Apply a tool call delta to a target object.
434+ * @param {object} target The target object to apply the delta to
435+ * @param {object} delta The delta object to apply
436+ */
437+ static #applyToolCallDelta(target, delta) {
438+ for (const key in delta) {
439+ if (!Object.prototype.hasOwnProperty.call(delta, key)) continue;
440+ if (key === '__proto__' || key === 'constructor') continue;
441+
442+ const deltaValue = delta[key];
443+ const targetValue = target[key];
444+
445+ if (deltaValue === null || deltaValue === undefined) {
446+ target[key] = deltaValue;
447+ continue;
448+ }
449+
450+ if (typeof deltaValue === 'string') {
451+ if (typeof targetValue === 'string') {
452+ // Concatenate strings
453+ target[key] = targetValue + deltaValue;
454+ } else {
455+ target[key] = deltaValue;
456+ }
457+ } else if (typeof deltaValue === 'object' && !Array.isArray(deltaValue)) {
458+ if (typeof targetValue !== 'object' || targetValue === null || Array.isArray(targetValue)) {
459+ target[key] = {};
460+ }
461+ // Recursively apply deltas to nested objects
462+ ToolManager.#applyToolCallDelta(target[key], deltaValue);
463+ } else {
464+ // Assign other types directly
465+ target[key] = deltaValue;
466+ }
467+ }
468+ }
469+
470+ /**
471+ * Checks if tool calling is supported for the current settings and generation type.
472+ * @returns {boolean} Whether tool calling is supported for the given type
473+ */
474+ static isToolCallingSupported() {
475+ if (main_api !== 'openai' || !oai_settings.function_calling) {
476+ return false;
477+ }
478+
479+ const supportedSources = [
480+ chat_completion_sources.OPENAI,
481+ chat_completion_sources.CUSTOM,
482+ chat_completion_sources.MISTRALAI,
483+ chat_completion_sources.CLAUDE,
484+ chat_completion_sources.OPENROUTER,
485+ chat_completion_sources.GROQ,
486+ ];
487+ return supportedSources.includes(oai_settings.chat_completion_source);
488+ }
489+
490+ /**
491+ * Checks if tool calls can be performed for the current settings and generation type.
492+ * @param {string} type Generation type
493+ * @returns {boolean} Whether tool calls can be performed for the given type
494+ */
495+ static canPerformToolCalls(type) {
496+ const noToolCallTypes = ['impersonate', 'quiet', 'continue'];
497+ const isSupported = ToolManager.isToolCallingSupported();
498+ return isSupported && !noToolCallTypes.includes(type);
499+ }
500+
501+ /**
502+ * Utility function to get tool calls from the response data.
503+ * @param {any} data Response data
504+ * @returns {any[]} Tool calls from the response data
505+ */
506+ static #getToolCallsFromData(data) {
507+ const isClaudeToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isClaudeToolCall) : c?.input && c?.name && c?.id;
508+ const convertClaudeToolCall = c => ({ id: c.id, function: { name: c.name, arguments: c.input } });
509+
510+ // Parsed tool calls from streaming data
511+ if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) {
512+ return isClaudeToolCall(data[0]) ? data[0].filter(x => x).map(convertClaudeToolCall) : data[0];
513+ }
514+
515+ // Parsed tool calls from non-streaming data
516+ if (Array.isArray(data?.choices)) {
517+ // Find a choice with 0-index
518+ const choice = data.choices.find(choice => choice.index === 0);
519+
520+ if (choice) {
521+ return choice.message.tool_calls;
522+ }
523+ }
524+
525+ // Claude tool calls to OpenAI tool calls
526+ if (Array.isArray(data?.content)) {
527+ const content = data.content.filter(c => c.type === 'tool_use').map(convertClaudeToolCall);
528+
529+ if (content) {
530+ return content;
531+ }
532+ }
533+ }
534+
535+ /**
536+ * Checks if the response data contains tool calls.
537+ * @param {object} data Response data
538+ * @returns {boolean} Whether the response data contains tool calls
539+ */
540+ static hasToolCalls(data) {
541+ const toolCalls = ToolManager.#getToolCallsFromData(data);
542+ return Array.isArray(toolCalls) && toolCalls.length > 0;
543+ }
544+
545+ /**
546+ * Check for function tool calls in the response data and invoke them.
547+ * @param {any} data Reply data
548+ * @returns {Promise<ToolInvocationResult>} Successful tool invocations
549+ */
550+ static async invokeFunctionTools(data) {
551+ /** @type {ToolInvocationResult} */
552+ const result = {
553+ invocations: [],
554+ errors: [],
555+ };
556+ const toolCalls = ToolManager.#getToolCallsFromData(data);
557+
558+ if (!Array.isArray(toolCalls)) {
559+ return result;
560+ }
561+
562+ for (const toolCall of toolCalls) {
563+ if (typeof toolCall.function !== 'object') {
564+ continue;
565+ }
566+
567+ console.log('Function tool call:', toolCall);
568+ const id = toolCall.id;
569+ const parameters = toolCall.function.arguments;
570+ const name = toolCall.function.name;
571+ const displayName = ToolManager.getDisplayName(name);
572+
573+ const message = ToolManager.formatToolCallMessage(name, parameters);
574+ const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 });
575+ const toolResult = await ToolManager.invokeFunctionTool(name, parameters);
576+ toastr.clear(toast);
577+ console.log('Function tool result:', result);
578+
579+ // Save a successful invocation
580+ if (toolResult instanceof Error) {
581+ result.errors.push(toolResult);
582+ continue;
583+ }
584+
585+ const invocation = {
586+ id,
587+ displayName,
588+ name,
589+ parameters: stringify(parameters),
590+ result: toolResult,
591+ };
592+ result.invocations.push(invocation);
593+ }
594+
595+ return result;
596+ }
597+
598+ /**
599+ * Groups tool names by count.
600+ * @param {string[]} toolNames Tool names
601+ * @returns {string} Grouped tool names
602+ */
603+ static #groupToolNames(toolNames) {
604+ const toolCounts = toolNames.reduce((acc, name) => {
605+ acc[name] = (acc[name] || 0) + 1;
606+ return acc;
607+ }, {});
608+ return Object.entries(toolCounts).map(([name, count]) => count > 1 ? `${name} (${count})` : name).join(', ');
609+ }
610+
611+ /**
612+ * Formats a message with tool invocations.
613+ * @param {ToolInvocation[]} invocations Tool invocations.
614+ * @returns {string} Formatted message with tool invocations.
615+ */
616+ static #formatToolInvocationMessage(invocations) {
617+ const data = structuredClone(invocations);
618+ const detailsElement = document.createElement('details');
619+ const summaryElement = document.createElement('summary');
620+ const preElement = document.createElement('pre');
621+ const codeElement = document.createElement('code');
622+ codeElement.classList.add('language-json');
623+ data.forEach(i => {
624+ i.parameters = tryParse(i.parameters);
625+ i.result = tryParse(i.result);
626+ });
627+ codeElement.textContent = JSON.stringify(data, null, 2);
628+ const toolNames = data.map(i => i.displayName || i.name);
629+ summaryElement.textContent = `Tool calls: ${this.#groupToolNames(toolNames)}`;
630+ preElement.append(codeElement);
631+ detailsElement.append(summaryElement, preElement);
632+ return detailsElement.outerHTML;
633+ }
634+
635+ /**
636+ * Saves function tool invocations to the last user chat message extra metadata.
637+ * @param {ToolInvocation[]} invocations Successful tool invocations
638+ */
639+ static async saveFunctionToolInvocations(invocations) {
640+ if (!Array.isArray(invocations) || invocations.length === 0) {
641+ return;
642+ }
643+ const message = {
644+ name: systemUserName,
645+ force_avatar: system_avatar,
646+ is_system: true,
647+ is_user: false,
648+ mes: ToolManager.#formatToolInvocationMessage(invocations),
649+ extra: {
650+ isSmallSys: true,
651+ tool_invocations: invocations,
652+ },
653+ };
654+ chat.push(message);
655+ await eventSource.emit(event_types.TOOL_CALLS_PERFORMED, invocations);
656+ addOneMessage(message);
657+ await eventSource.emit(event_types.TOOL_CALLS_RENDERED, invocations);
658+ await saveChatConditional();
659+ }
660+
661+ /**
662+ * Shows an error message for tool calls.
663+ * @param {Error[]} errors Errors that occurred during tool invocation
664+ * @returns {void}
665+ */
666+ static showToolCallError(errors) {
667+ toastr.error('An error occurred while invoking function tools. Click here for more details.', 'Tool Calling', {
668+ onclick: () => Popup.show.text('Tool Calling Errors', DOMPurify.sanitize(errors.map(e => `${e.cause}: ${e.message}`).join('<br>'))),
669+ timeOut: 5000,
670+ });
671+ }
672+
673+ static initToolSlashCommands() {
674+ const toolsEnumProvider = () => ToolManager.tools.map(tool => {
675+ const toolOpenAI = tool.toFunctionOpenAI();
676+ return new SlashCommandEnumValue(toolOpenAI.function.name, toolOpenAI.function.description, enumTypes.enum, enumIcons.closure);
677+ });
678+
679+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
680+ name: 'tools-list',
681+ aliases: ['tool-list'],
682+ helpString: 'Gets a list of all registered tools in the OpenAI function JSON format. Use the <code>return</code> argument to specify the return value type.',
683+ returns: 'A list of all registered tools.',
684+ namedArgumentList: [
685+ SlashCommandNamedArgument.fromProps({
686+ name: 'return',
687+ description: 'The way how you want the return value to be provided',
688+ typeList: [ARGUMENT_TYPE.STRING],
689+ defaultValue: 'none',
690+ enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
691+ forceEnum: true,
692+ }),
693+ ],
694+ callback: async (args) => {
695+ /** @type {any} */
696+ const returnType = String(args?.return ?? 'popup-html').trim().toLowerCase();
697+ const objectToStringFunc = (tools) => Array.isArray(tools) ? tools.map(x => x.toString()).join('\n\n') : tools.toString();
698+ const tools = ToolManager.tools.map(tool => tool.toFunctionOpenAI());
699+ return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', tools ?? [], { objectToStringFunc });
700+ },
701+ }));
702+
703+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
704+ name: 'tools-invoke',
705+ aliases: ['tool-invoke'],
706+ helpString: 'Invokes a registered tool by name. The <code>parameters</code> argument MUST be a JSON-serialized object.',
707+ namedArgumentList: [
708+ SlashCommandNamedArgument.fromProps({
709+ name: 'parameters',
710+ description: 'The parameters to pass to the tool.',
711+ typeList: [ARGUMENT_TYPE.DICTIONARY],
712+ isRequired: true,
713+ acceptsMultiple: false,
714+ }),
715+ ],
716+ unnamedArgumentList: [
717+ SlashCommandArgument.fromProps({
718+ description: 'The name of the tool to invoke.',
719+ typeList: [ARGUMENT_TYPE.STRING],
720+ isRequired: true,
721+ acceptsMultiple: false,
722+ forceEnum: true,
723+ enumProvider: toolsEnumProvider,
724+ }),
725+ ],
726+ callback: async (args, name) => {
727+ const { parameters } = args;
728+
729+ const result = await ToolManager.invokeFunctionTool(String(name), parameters);
730+ if (result instanceof Error) {
731+ throw result;
732+ }
733+
734+ return result;
735+ },
736+ }));
737+
738+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
739+ name: 'tools-register',
740+ aliases: ['tool-register'],
741+ helpString: `<div>Registers a new tool with the tool registry.</div>
742+ <ul>
743+ <li>The <code>parameters</code> argument MUST be a JSON-serialized object with a valid JSON schema.</li>
744+ <li>The unnamed argument MUST be a closure that accepts the function parameters as local script variables.</li>
745+ </ul>
746+ <div>See <a target="_blank" href="https://json-schema.org/learn/">json-schema.org</a> and <a target="_blank" href="https://platform.openai.com/docs/guides/function-calling">OpenAI Function Calling</a> for more information.</div>
747+ <div>Example:</div>
748+ <pre><code>/let key=echoSchema
749+{
750+ "$schema": "http://json-schema.org/draft-04/schema#",
751+ "type": "object",
752+ "properties": {
753+ "message": {
754+ "type": "string",
755+ "description": "The message to echo."
756+ }
757+ },
758+ "required": [
759+ "message"
760+ ]
761+}
762+||
763+/tools-register name=Echo description="Echoes a message. Call when the user is asking to repeat something" parameters={{var::echoSchema}} {: /echo {{var::arg.message}} :}</code></pre>`,
764+ namedArgumentList: [
765+ SlashCommandNamedArgument.fromProps({
766+ name: 'name',
767+ description: 'The name of the tool.',
768+ typeList: [ARGUMENT_TYPE.STRING],
769+ isRequired: true,
770+ acceptsMultiple: false,
771+ }),
772+ SlashCommandNamedArgument.fromProps({
773+ name: 'description',
774+ description: 'A description of what the tool does.',
775+ typeList: [ARGUMENT_TYPE.STRING],
776+ isRequired: true,
777+ acceptsMultiple: false,
778+ }),
779+ SlashCommandNamedArgument.fromProps({
780+ name: 'parameters',
781+ description: 'The parameters for the tool.',
782+ typeList: [ARGUMENT_TYPE.DICTIONARY],
783+ isRequired: true,
784+ acceptsMultiple: false,
785+ }),
786+ SlashCommandNamedArgument.fromProps({
787+ name: 'displayName',
788+ description: 'The display name of the tool.',
789+ typeList: [ARGUMENT_TYPE.STRING],
790+ isRequired: false,
791+ acceptsMultiple: false,
792+ }),
793+ SlashCommandNamedArgument.fromProps({
794+ name: 'formatMessage',
795+ description: 'The closure to be executed to format the tool call message. Must return a string.',
796+ typeList: [ARGUMENT_TYPE.CLOSURE],
797+ isRequired: true,
798+ acceptsMultiple: false,
799+ }),
800+ ],
801+ unnamedArgumentList: [
802+ SlashCommandArgument.fromProps({
803+ description: 'The closure to be executed when the tool is invoked.',
804+ typeList: [ARGUMENT_TYPE.CLOSURE],
805+ isRequired: true,
806+ acceptsMultiple: false,
807+ }),
808+ ],
809+ callback: async (args, action) => {
810+ /**
811+ * Converts a slash command closure to a function.
812+ * @param {SlashCommandClosure} action Closure to convert to a function
813+ * @returns {function} Function that executes the closure
814+ */
815+ function closureToFunction(action) {
816+ return async (args) => {
817+ const localClosure = action.getCopy();
818+ localClosure.onProgress = () => { };
819+ const scope = localClosure.scope;
820+ if (typeof args === 'object' && args !== null) {
821+ assignNestedVariables(scope, args, 'arg');
822+ } else if (typeof args !== 'undefined') {
823+ scope.letVariable('arg', args);
824+ }
825+ const result = await localClosure.execute();
826+ return result.pipe;
827+ };
828+ }
829+
830+ const { name, displayName, description, parameters, formatMessage } = args;
831+
832+ if (!(action instanceof SlashCommandClosure)) {
833+ throw new Error('The unnamed argument must be a closure.');
834+ }
835+ if (typeof name !== 'string' || !name) {
836+ throw new Error('The "name" argument must be a non-empty string.');
837+ }
838+ if (typeof description !== 'string' || !description) {
839+ throw new Error('The "description" argument must be a non-empty string.');
840+ }
841+ if (typeof parameters !== 'string' || !isJson(parameters)) {
842+ throw new Error('The "parameters" argument must be a JSON-serialized object.');
843+ }
844+ if (displayName && typeof displayName !== 'string') {
845+ throw new Error('The "displayName" argument must be a string.');
846+ }
847+ if (formatMessage && !(formatMessage instanceof SlashCommandClosure)) {
848+ throw new Error('The "formatMessage" argument must be a closure.');
849+ }
850+
851+ const actionFunc = closureToFunction(action);
852+ const formatMessageFunc = formatMessage instanceof SlashCommandClosure ? closureToFunction(formatMessage) : null;
853+
854+ ToolManager.registerFunctionTool({
855+ name: String(name ?? ''),
856+ displayName: String(displayName ?? ''),
857+ description: String(description ?? ''),
858+ parameters: JSON.parse(parameters ?? '{}'),
859+ action: actionFunc,
860+ formatMessage: formatMessageFunc,
861+ });
862+
863+ return '';
864+ },
865+ }));
866+
867+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
868+ name: 'tools-unregister',
869+ aliases: ['tool-unregister'],
870+ helpString: 'Unregisters a tool from the tool registry.',
871+ unnamedArgumentList: [
872+ SlashCommandArgument.fromProps({
873+ description: 'The name of the tool to unregister.',
874+ typeList: [ARGUMENT_TYPE.STRING],
875+ isRequired: true,
876+ acceptsMultiple: false,
877+ forceEnum: true,
878+ enumProvider: toolsEnumProvider,
879+ }),
880+ ],
881+ callback: async (name) => {
882+ if (typeof name !== 'string' || !name) {
883+ throw new Error('The unnamed argument must be a non-empty string.');
884+ }
885+
886+ ToolManager.unregisterFunctionTool(name);
887+ return '';
888+ },
889+ }));
890+ }
891+}
public/style.css+10 -0
@@ -424,6 +424,16 @@ small {
424424 text-align: center;
425425}
426426
427+.mes.smallSysMes pre {
428+ text-align: initial;
429+ word-break: break-all;
430+ margin-top: 5px;
431+}
432+
433+.mes.smallSysMes summary {
434+ cursor: pointer;
435+}
436+
427437.mes.smallSysMes .mes_text p:last-child {
428438 margin: 0;
429439}
src/endpoints/backends/chat-completions.js+23 -31
@@ -4,7 +4,7 @@ const fetch = require('node-fetch').default;
44const { jsonParser } = require('../../express-common');
55const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');
66const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
77const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertCohereToolsconvertAI21Messages, convertAI21MessagesmergeMessages } = require('../../prompt-converters');
88const CohereStream = require('../../cohere-stream');
99
1010const { readSecret, SECRET_KEYS } = require('../secrets');
@@ -31,8 +31,11 @@ const API_AI21 = 'https://api.ai21.com/studio/v1';
3131 */
3232function postProcessPrompt(messages, type, charName, userName) {
3333 switch (type) {
34+ case 'merge':
3435 case 'claude':
3536 return convertClaudeMessagesmergeMessages(messages, '', false, '', charName, userName, false).messages;
37+ case 'strict':
38+ return mergeMessages(messages, charName, userName, true);
3639 default:
3740 return messages;
3841 }
@@ -84,7 +87,7 @@ async function sendClaudeRequest(request, response) {
8487 const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();
8588 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);
8689 const divider = '-'.repeat(process.stdout.columns);
8790 const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false) && request.body.model.startsWith('claude-3');
8891
8992 if (!apiKey) {
9093 console.log(color.red(`Claude API key is missing.\n${divider}`));
@@ -98,8 +101,9 @@ async function sendClaudeRequest(request, response) {
98101 controller.abort();
99102 });
100103 const additionalHeaders = {};
104+ const useTools = request.body.model.startsWith('claude-3') && Array.isArray(request.body.tools) && request.body.tools.length > 0;
101105 const useSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;
102106 const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, request.body.human_sysprompt_message, request.body.char_name, request.body.user_name);
103107 // Add custom stop sequences
104108 const stopSequences = [];
105109 if (Array.isArray(request.body.stop)) {
@@ -107,7 +111,7 @@ async function sendClaudeRequest(request, response) {
107111 }
108112
109113 const requestBody = {
110114 /** @type {any} */ system: ''[],
111115 messages: convertedPrompt.messages,
112116 model: request.body.model,
113117 max_tokens: request.body.max_tokens,
@@ -118,23 +122,29 @@ async function sendClaudeRequest(request, response) {
118122 stream: request.body.stream,
119123 };
120124 if (useSystemPrompt) {
121- requestBody.system = enableSystemPromptCache
125+ if (enableSystemPromptCache && Array.isArray(convertedPrompt.systemPrompt) && convertedPrompt.systemPrompt.length) {
122126 ? convertedPrompt.systemPrompt[{ type: 'text', text: convertedPrompt.systemPrompt,.length - 1]['cache_control:'] = { type: 'ephemeral' } }];
123- : convertedPrompt.systemPrompt;
127+ }
128+
129+ requestBody.system = convertedPrompt.systemPrompt;
124130 } else {
125131 delete requestBody.system;
126132 }
127- if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
133+ if (useTools) {
128134 // Claude doesn't do prefills on function calls, and doesn't allow empty messages
129135 if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {
130136 convertedPrompt.messages.push({ role: 'user', content: '.' });
131137 }
132138 additionalHeaders['anthropic-beta'] = 'tools-2024-05-16';
133139 requestBody.tool_choice = { type: request.body.tool_choice === 'required' ? 'any' : 'auto' };
134140 requestBody.tools = request.body.tools
135141 .filter(tool => tool.type === 'function')
136142 .map(tool => tool.function)
137143 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
144+
145+ if (enableSystemPromptCache && requestBody.tools.length) {
146+ requestBody.tools[requestBody.tools.length - 1]['cache_control'] = { type: 'ephemeral' };
147+ }
138148 }
139149 if (enableSystemPromptCache) {
140150 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
@@ -483,7 +493,7 @@ async function sendMistralAIRequest(request, response) {
483493
484494 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
485495 requestBody['tools'] = request.body.tools;
486496 requestBody['tool_choice'] = request.body.tool_choice === 'required' ? 'any' : 'auto';
487497 }
488498
489499 const config = {
@@ -553,12 +563,6 @@ async function sendCohereRequest(request, response) {
553563 });
554564 }
555565
556- if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
557- tools.push(...convertCohereTools(request.body.tools));
558- // Can't have both connectors and tools in the same request
559- connectors.splice(0, connectors.length);
560- }
561-
562566 // https://docs.cohere.com/reference/chat
563567 const requestBody = {
564568 stream: Boolean(request.body.stream),
@@ -908,24 +912,12 @@ router.post('/generate', jsonParser, function (request, response) {
908912 apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);
909913 headers = {};
910914 bodyParams = {};
911915 request.body.messages = postProcessPrompt(request.body.messages, 'claudestrict', request.body.char_name, request.body.user_name);
912916 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {
913917 apiUrl = API_GROQ;
914918 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
915919 headers = {};
916920 bodyParams = {};
917-
918- // 'required' tool choice is not supported by Groq
919- if (request.body.tool_choice === 'required') {
920- if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
921- request.body.tool_choice = request.body.tools.length > 1
922- ? 'auto' :
923- { type: 'function', function: { name: request.body.tools[0]?.function?.name } };
924-
925- } else {
926- request.body.tool_choice = 'none';
927- }
928- }
929921 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZEROONEAI) {
930922 apiUrl = API_01AI;
931923 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
@@ -962,7 +954,7 @@ router.post('/generate', jsonParser, function (request, response) {
962954 controller.abort();
963955 });
964956
965- if (!isTextCompletion) {
957+ if (!isTextCompletion && Array.isArray(request.body.tools) && request.body.tools.length > 0) {
966958 bodyParams['tools'] = request.body.tools;
967959 bodyParams['tool_choice'] = request.body.tool_choice;
968960 }
src/endpoints/novelai.js+1 -1
@@ -51,7 +51,7 @@ const eratoRepPenWhitelist = [
5151 6, 1, 11, 13, 25, 198, 12, 9, 8, 279, 264, 459, 323, 477, 539, 912, 374, 574, 1051, 1550, 1587, 4536, 5828, 15058,
5252 3287, 3250, 1461, 1077, 813, 11074, 872, 1202, 1436, 7846, 1288, 13434, 1053, 8434, 617, 9167, 1047, 19117, 706,
5353 12775, 649, 4250, 527, 7784, 690, 2834, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 1210, 1359, 608, 220, 596, 956,
5454 3077, 44886, 4265, 3358, 2351, 2846, 311, 389, 315, 304, 520, 505, 430,
5555];
5656
5757// Ban the dinkus and asterism
src/endpoints/search.js+70 -49
@@ -22,58 +22,15 @@ const visitHeaders = {
2222 'Sec-Fetch-User': '?1',
2323};
2424
25-router.post('/serpapi', jsonParser, async (request, response) => {
26- try {
27- const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);
28-
29- if (!key) {
30- console.log('No SerpApi key found');
31- return response.sendStatus(400);
32- }
33-
34- const { query } = request.body;
35- const result = await fetch(`https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${key}`);
36-
37- console.log('SerpApi query', query);
38-
39- if (!result.ok) {
40- const text = await result.text();
41- console.log('SerpApi request failed', result.statusText, text);
42- return response.status(500).send(text);
43- }
44-
45- const data = await result.json();
46- return response.json(data);
47- } catch (error) {
48- console.log(error);
49- return response.sendStatus(500);
50- }
51-});
52-
5325/**
5426 * GetExtract the transcript of a YouTube video
55- * @copyright https://github.com/Kakulukian/youtube-transcript (MIT License)
27+ * @param {string} videoPageBody HTML of the video page
28+ * @param {string} lang Language code
29+ * @returns {Promise<string>} Transcript text
5630 */
57-router.post('/transcript', jsonParser, async (request, response) => {
31+async function extractTranscript(videoPageBody, lang) {
58- try {
5932 const he = require('he');
6033 const RE_XML_TRANSCRIPT = /<text start="([^"]*)" dur="([^"]*)">([^<]*)<\/text>/g;
61- const id = request.body.id;
62- const lang = request.body.lang;
63-
64- if (!id) {
65- console.log('Id is required for /transcript');
66- return response.sendStatus(400);
67- }
68-
69- const videoPageResponse = await fetch(`https://www.youtube.com/watch?v=${id}`, {
70- headers: {
71- ...(lang && { 'Accept-Language': lang }),
72- 'User-Agent': visitHeaders['User-Agent'],
73- },
74- });
75-
76- const videoPageBody = await videoPageResponse.text();
7734 const splittedHTML = videoPageBody.split('"captions":');
7835
7936 if (splittedHTML.length <= 1) {
@@ -128,8 +85,72 @@ router.post('/transcript', jsonParser, async (request, response) => {
12885 }));
12986 // The text is double-encoded
13087 const transcriptText = transcript.map((line) => he.decode(he.decode(line.text))).join(' ');
88+ return transcriptText;
89+}
90+
91+router.post('/serpapi', jsonParser, async (request, response) => {
92+ try {
93+ const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);
94+
95+ if (!key) {
96+ console.log('No SerpApi key found');
97+ return response.sendStatus(400);
98+ }
99+
100+ const { query } = request.body;
101+ const result = await fetch(`https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${key}`);
102+
103+ console.log('SerpApi query', query);
104+
105+ if (!result.ok) {
106+ const text = await result.text();
107+ console.log('SerpApi request failed', result.statusText, text);
108+ return response.status(500).send(text);
109+ }
110+
111+ const data = await result.json();
112+ return response.json(data);
113+ } catch (error) {
114+ console.log(error);
115+ return response.sendStatus(500);
116+ }
117+});
118+
119+/**
120+ * Get the transcript of a YouTube video
121+ * @copyright https://github.com/Kakulukian/youtube-transcript (MIT License)
122+ */
123+router.post('/transcript', jsonParser, async (request, response) => {
124+ try {
125+ const id = request.body.id;
126+ const lang = request.body.lang;
127+ const json = request.body.json;
128+
129+ if (!id) {
130+ console.log('Id is required for /transcript');
131+ return response.sendStatus(400);
132+ }
131133
132- return response.send(transcriptText);
134+ const videoPageResponse = await fetch(`https://www.youtube.com/watch?v=${id}`, {
135+ headers: {
136+ ...(lang && { 'Accept-Language': lang }),
137+ 'User-Agent': visitHeaders['User-Agent'],
138+ },
139+ });
140+
141+ const videoPageBody = await videoPageResponse.text();
142+
143+ try {
144+ const transcriptText = await extractTranscript(videoPageBody, lang);
145+ return json
146+ ? response.json({ transcript: transcriptText, html: videoPageBody })
147+ : response.send(transcriptText);
148+ } catch (error) {
149+ if (json) {
150+ return response.json({ html: videoPageBody, transcript: '' });
151+ }
152+ throw error;
153+ }
133154 } catch (error) {
134155 console.log(error);
135156 return response.sendStatus(500);
src/prompt-converters.js+237 -119
@@ -1,5 +1,8 @@
11require('./polyfill.js');
22const { getConfigValue } = require('./util.js');
3+const crypto = require('crypto');
4+
5+const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');
36
47/**
58 * Convert a prompt from the ChatML objects to the format used by Claude.
@@ -19,6 +22,14 @@ function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill,
1922 //Prepare messages for claude.
2023 //When 'Exclude Human/Assistant prefixes' checked, setting messages role to the 'system'(last message is exception).
2124 if (messages.length > 0) {
25+ messages.forEach((m) => {
26+ if (!m.content) {
27+ m.content = '';
28+ }
29+ if (m.tool_calls) {
30+ m.content += JSON.stringify(m.tool_calls);
31+ }
32+ });
2233 if (excludePrefixes) {
2334 messages.slice(0, -1).forEach(message => message.role = 'system');
2435 } else {
@@ -80,12 +91,13 @@ function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill,
8091 * @param {object[]} messages Array of messages
8192 * @param {string} prefillString User determined prefill string
8293 * @param {boolean} useSysPrompt See if we want to use a system prompt
94+ * @param {boolean} useTools See if we want to use tools
8395 * @param {string} humanMsgFix Add Human message between system prompt and assistant.
8496 * @param {string} charName Character name
8597 * @param {string} userName User name
8698 */
8799function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools, humanMsgFix, charName = '', userName = '') {
88100 let systemPrompt = ''[];
89101 if (useSysPrompt) {
90102 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
91103 let i;
@@ -104,7 +116,7 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
104116 messages[i].content = `${charName}: ${messages[i].content}`;
105117 }
106118 }
107- systemPrompt += `${messages[i].content}\n\n`;
119+ systemPrompt.push({ type: 'text', text: messages[i].content });
108120 }
109121
110122 messages.splice(0, i);
@@ -114,12 +126,32 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
114126 if (messages.length === 0 || (messages.length > 0 && messages[0].role !== 'user')) {
115127 messages.unshift({
116128 role: 'user',
117129 content: humanMsgFix || '[Start a new chat]'PROMPT_PLACEHOLDER,
118130 });
119131 }
120132 }
133+
121134 // Now replace all further messages that have the role 'system' with the role 'user'. (or all if we're not using one)
135+ const parse = (str) => typeof str === 'string' ? JSON.parse(str) : str;
122136 messages.forEach((message) => {
137+ if (message.role === 'assistant' && message.tool_calls) {
138+ message.content = message.tool_calls.map((tc) => ({
139+ type: 'tool_use',
140+ id: tc.id,
141+ name: tc.function.name,
142+ input: parse(tc.function.arguments),
143+ }));
144+ }
145+
146+ if (message.role === 'tool') {
147+ message.role = 'user';
148+ message.content = [{
149+ type: 'tool_result',
150+ tool_use_id: message.tool_call_id,
151+ content: message.content,
152+ }];
153+ }
154+
123155 if (message.role === 'system') {
124156 if (userName && message.name === 'example_user') {
125157 message.content = `${userName}: ${message.content}`;
@@ -128,68 +160,116 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
128160 message.content = `${charName}: ${message.content}`;
129161 }
130162 message.role = 'user';
163+
164+ // Delete name here so it doesn't get added later
165+ delete message.name;
131166 }
132- });
133167
134- // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
168+ // Convert everything to an array of it would be easier to work with
135- if (prefillString) {
169+ if (typeof message.content === 'string') {
136- messages.push({
170+ // Take care of name properties since claude messages don't support them
137- role: 'assistant',
171+ if (message.name) {
138- content: prefillString.trimEnd(),
172+ message.content = `${message.name}: ${message.content}`;
139- });
140173 }
141174
142- // Since the messaging endpoint only supports user assistant roles in turns, we have to merge messages with the same role if they follow eachother
175+ message.content = [{ type: 'text', text: message.content }];
143- // Also handle multi-modality, holy slop.
176+ } else if (Array.isArray(message.content)) {
144- let mergedMessages = [];
177+ message.content = message.content.map((content) => {
145- messages.forEach((message) => {
178+ if (content.type === 'image_url') {
146179 const imageEntry = message.content?.[1]?.image_url;
147180 const imageData = imageEntry?.url;
148181 const mimeType = imageData?.split(';')?.[0].split(':')?.[1];
149182 const base64Data = imageData?.split(',')?.[1];
150183
151- // Take care of name properties since claude messages don't support them
184+ return {
185+ type: 'image',
186+ source: {
187+ type: 'base64',
188+ media_type: mimeType,
189+ data: base64Data,
190+ },
191+ };
192+ }
193+
194+ if (content.type === 'text') {
152195 if (message.name) {
153- if (Array.isArray(message.content)) {
196+ content.text = `${message.name}: ${content.text}`;
154- message.content[0].text = `${message.name}: ${message.content[0].text}`;
197+ }
155- } else {
198+
156- message.content = `${message.name}: ${message.content}`;
199+ return content;
200+ }
201+
202+ return content;
203+ });
157204 }
205+
206+ // Remove offending properties
158207 delete message.name;
208+ delete message.tool_calls;
209+ delete message.tool_call_id;
210+ });
211+
212+ // Images in assistant messages should be moved to the next user message
213+ for (let i = 0; i < messages.length; i++) {
214+ if (messages[i].role === 'assistant' && messages[i].content.some(c => c.type === 'image')) {
215+ // Find the next user message
216+ let j = i + 1;
217+ while (j < messages.length && messages[j].role !== 'user') {
218+ j++;
159219 }
160220
161- if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {
221+ // Move the images
162222 if (Array.isArray(messagej >= messages.content)length) {
163- if (Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {
223+ // If there is no user message after the assistant message, add a new one
164- mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content[0].text;
224+ messages.splice(i + 1, 0, { role: 'user', content: [] });
165- } else {
166- mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content[0].text;
167225 }
168- } else {
226+
169- if (Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {
227+ messages[j].content.push(...messages[i].content.filter(c => c.type === 'image'));
170- mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content;
228+ messages[i].content = messages[i].content.filter(c => c.type !== 'image');
171- } else {
172- mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
173229 }
174230 }
231+
232+ // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
233+ if (prefillString) {
234+ messages.push({
235+ role: 'assistant',
236+ // Dangling whitespace are not allowed for prefilling
237+ content: [{ type: 'text', text: prefillString.trimEnd() }],
238+ });
239+ }
240+
241+ // Since the messaging endpoint only supports user assistant roles in turns, we have to merge messages with the same role if they follow eachother
242+ // Also handle multi-modality, holy slop.
243+ let mergedMessages = [];
244+ messages.forEach((message) => {
245+ if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {
246+ mergedMessages[mergedMessages.length - 1].content.push(...message.content);
175247 } else {
176248 mergedMessages.push(message);
177249 }
178- if (imageData) {
250+ });
179- mergedMessages[mergedMessages.length - 1].content = [
251+
180- { type: 'text', text: mergedMessages[mergedMessages.length - 1].content[0]?.text || mergedMessages[mergedMessages.length - 1].content },
252+ if (!useTools) {
181- {
253+ mergedMessages.forEach((message) => {
182- type: 'image', source: {
254+ message.content.forEach((content) => {
183- type: 'base64',
255+ if (content.type === 'tool_use') {
184- media_type: mimeType,
256+ content.type = 'text';
185- data: base64Data,
257+ content.text = JSON.stringify(content.input);
186- },
258+ delete content.id;
187- },
259+ delete content.name;
188- ];
260+ delete content.input;
261+ }
262+ if (content.type === 'tool_result') {
263+ content.type = 'text';
264+ content.text = content.content;
265+ delete content.tool_use_id;
266+ delete content.content;
189267 }
190268 });
269+ });
270+ }
191271
192272 return { messages: mergedMessages, systemPrompt: systemPrompt.trim() };
193273}
194274
195275/**
@@ -205,7 +285,6 @@ function convertCohereMessages(messages, charName = '', userName = '') {
205285 'user': 'USER',
206286 'assistant': 'CHATBOT',
207287 };
208- const placeholder = '[Start a new chat]';
209288 let systemPrompt = '';
210289
211290 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
@@ -233,12 +312,12 @@ function convertCohereMessages(messages, charName = '', userName = '') {
233312 if (messages.length === 0) {
234313 messages.unshift({
235314 role: 'user',
236315 content: placeholderPROMPT_PLACEHOLDER,
237316 });
238317 }
239318
240319 const lastNonSystemMessageIndex = messages.findLastIndex(msg => msg.role === 'user' || msg.role === 'assistant');
241320 const userPrompt = messages.slice(lastNonSystemMessageIndex).map(msg => msg.content).join('\n\n') || placeholderPROMPT_PLACEHOLDER;
242321
243322 const chatHistory = messages.slice(0, lastNonSystemMessageIndex).map(msg => {
244323 return {
@@ -414,7 +493,7 @@ function convertAI21Messages(messages, charName = '', userName = '') {
414493 if (messages.length === 0) {
415494 messages.unshift({
416495 role: 'user',
417- content: '[Start a new chat]',
496+ content: PROMPT_PLACEHOLDER,
418497 });
419498 }
420499
@@ -466,8 +545,18 @@ function convertMistralMessages(messages, charName = '', userName = '') {
466545 lastMsg.prefix = true;
467546 }
468547
548+ const sanitizeToolId = (id) => crypto.createHash('sha512').update(id).digest('hex').slice(0, 9);
549+
469550 // Doesn't support completion names, so prepend if not already done by the frontend (e.g. for group chats).
470551 messages.forEach(msg => {
552+ if ('tool_calls' in msg && Array.isArray(msg.tool_calls)) {
553+ msg.tool_calls.forEach(tool => {
554+ tool.id = sanitizeToolId(tool.id);
555+ });
556+ }
557+ if ('tool_call_id' in msg && msg.role === 'tool') {
558+ msg.tool_call_id = sanitizeToolId(msg.tool_call_id);
559+ }
471560 if (msg.role === 'system' && msg.name === 'example_assistant') {
472561 if (charName && !msg.content.startsWith(`${charName}: `)) {
473562 msg.content = `${charName}: ${msg.content}`;
@@ -488,6 +577,28 @@ function convertMistralMessages(messages, charName = '', userName = '') {
488577 }
489578 });
490579
580+ // If user role message immediately follows a tool message, append it to the last user message
581+ const fixToolMessages = () => {
582+ let rerun = true;
583+ while (rerun) {
584+ rerun = false;
585+ messages.forEach((message, i) => {
586+ if (i === messages.length - 1) {
587+ return;
588+ }
589+ if (message.role === 'tool' && messages[i + 1].role === 'user') {
590+ const lastUserMessage = messages.slice(0, i).findLastIndex(m => m.role === 'user' && m.content);
591+ if (lastUserMessage !== -1) {
592+ messages[lastUserMessage].content += '\n\n' + messages[i + 1].content;
593+ messages.splice(i + 1, 1);
594+ rerun = true;
595+ }
596+ }
597+ });
598+ }
599+ };
600+ fixToolMessages();
601+
491602 // If system role message immediately follows an assistant message, change its role to user
492603 for (let i = 0; i < messages.length - 1; i++) {
493604 if (messages[i].role === 'assistant' && messages[i + 1].role === 'system') {
@@ -499,6 +610,83 @@ function convertMistralMessages(messages, charName = '', userName = '') {
499610}
500611
501612/**
613+ * Merge messages with the same consecutive role, removing names if they exist.
614+ * @param {any[]} messages Messages to merge
615+ * @param {string} charName Character name
616+ * @param {string} userName User name
617+ * @param {boolean} strict Enable strict mode: only allow one system message at the start, force user first message
618+ * @returns {any[]} Merged messages
619+ */
620+function mergeMessages(messages, charName, userName, strict) {
621+ let mergedMessages = [];
622+
623+ // Remove names from the messages
624+ messages.forEach((message) => {
625+ if (!message.content) {
626+ message.content = '';
627+ }
628+ if (message.role === 'system' && message.name === 'example_assistant') {
629+ if (charName && !message.content.startsWith(`${charName}: `)) {
630+ message.content = `${charName}: ${message.content}`;
631+ }
632+ }
633+ if (message.role === 'system' && message.name === 'example_user') {
634+ if (userName && !message.content.startsWith(`${userName}: `)) {
635+ message.content = `${userName}: ${message.content}`;
636+ }
637+ }
638+ if (message.name && message.role !== 'system') {
639+ if (!message.content.startsWith(`${message.name}: `)) {
640+ message.content = `${message.name}: ${message.content}`;
641+ }
642+ }
643+ if (message.role === 'tool') {
644+ message.role = 'user';
645+ }
646+ delete message.name;
647+ delete message.tool_calls;
648+ delete message.tool_call_id;
649+ });
650+
651+ // Squash consecutive messages with the same role
652+ messages.forEach((message) => {
653+ if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role && message.content) {
654+ mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
655+ } else {
656+ mergedMessages.push(message);
657+ }
658+ });
659+
660+ // Prevent erroring out if the messages array is empty.
661+ if (messages.length === 0) {
662+ messages.unshift({
663+ role: 'user',
664+ content: PROMPT_PLACEHOLDER,
665+ });
666+ }
667+
668+ if (strict) {
669+ for (let i = 0; i < mergedMessages.length; i++) {
670+ // Force mid-prompt system messages to be user messages
671+ if (i > 0 && mergedMessages[i].role === 'system') {
672+ mergedMessages[i].role = 'user';
673+ }
674+ }
675+ if (mergedMessages.length) {
676+ if (mergedMessages[0].role === 'system' && (mergedMessages.length === 1 || mergedMessages[1].role !== 'user')) {
677+ mergedMessages.splice(1, 0, { role: 'user', content: PROMPT_PLACEHOLDER });
678+ }
679+ else if (mergedMessages[0].role !== 'system' && mergedMessages[0].role !== 'user') {
680+ mergedMessages.unshift({ role: 'user', content: PROMPT_PLACEHOLDER });
681+ }
682+ }
683+ return mergeMessages(mergedMessages, charName, userName, false);
684+ }
685+
686+ return mergedMessages;
687+}
688+
689+/**
502690 * Convert a prompt from the ChatML objects to the format used by Text Completion API.
503691 * @param {object[]} messages Array of messages
504692 * @returns {string} Prompt for Text Completion API
@@ -523,76 +711,6 @@ function convertTextCompletionPrompt(messages) {
523711 return messageStrings.join('\n') + '\nassistant:';
524712}
525713
526-/**
527- * Convert OpenAI Chat Completion tools to the format used by Cohere.
528- * @param {object[]} tools OpenAI Chat Completion tool definitions
529- */
530-function convertCohereTools(tools) {
531- if (!Array.isArray(tools) || tools.length === 0) {
532- return [];
533- }
534-
535- const jsonSchemaToPythonTypes = {
536- 'string': 'str',
537- 'number': 'float',
538- 'integer': 'int',
539- 'boolean': 'bool',
540- 'array': 'list',
541- 'object': 'dict',
542- };
543-
544- const cohereTools = [];
545-
546- for (const tool of tools) {
547- if (tool?.type !== 'function') {
548- console.log(`Unsupported tool type: ${tool.type}`);
549- continue;
550- }
551-
552- const name = tool?.function?.name;
553- const description = tool?.function?.description;
554- const properties = tool?.function?.parameters?.properties;
555- const required = tool?.function?.parameters?.required;
556- const parameters = {};
557-
558- if (!name) {
559- console.log('Tool name is missing');
560- continue;
561- }
562-
563- if (!description) {
564- console.log('Tool description is missing');
565- }
566-
567- if (!properties || typeof properties !== 'object') {
568- console.log(`No properties found for tool: ${tool?.function?.name}`);
569- continue;
570- }
571-
572- for (const property in properties) {
573- const parameterDefinition = properties[property];
574- const description = parameterDefinition.description || (parameterDefinition.enum ? JSON.stringify(parameterDefinition.enum) : '');
575- const type = jsonSchemaToPythonTypes[parameterDefinition.type] || 'str';
576- const isRequired = Array.isArray(required) && required.includes(property);
577- parameters[property] = {
578- description: description,
579- type: type,
580- required: isRequired,
581- };
582- }
583-
584- const cohereTool = {
585- name: tool.function.name,
586- description: tool.function.description,
587- parameter_definitions: parameters,
588- };
589-
590- cohereTools.push(cohereTool);
591- }
592-
593- return cohereTools;
594-}
595-
596714module.exports = {
597715 convertClaudePrompt,
598716 convertClaudeMessages,
@@ -600,6 +718,6 @@ module.exports = {
600718 convertTextCompletionPrompt,
601719 convertCohereMessages,
602720 convertMistralMessages,
603- convertCohereTools,
604721 convertAI21Messages,
722+ mergeMessages,
605723};