Merge pull request #2976 from SillyTavern/cc-async-count Chat Completion: switch to async token handling

346e77ecfc4fd38ef9442aa38db499062dd7d9c4

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

Signed
2 files changed, +118 -72Ignore whitespace
public/scripts/PromptManager.js+1 -1
@@ -315,7 +315,7 @@ class PromptManager {
315315 */
316316 init(moduleConfiguration, serviceSettings) {
317317 this.configuration = Object.assign(this.configuration, moduleConfiguration);
318318 this.tokenHandler = this.tokenHandler || new TokenHandler(() => { throw new Error('Token handler not set'); });
319319 this.serviceSettings = serviceSettings;
320320 this.containerElement = document.getElementById(this.configuration.containerIdentifier);
321321
public/scripts/openai.js+117 -71
@@ -60,7 +60,7 @@ import {
6060 resetScrollHeight,
6161 stringFormat,
6262} from './utils.js';
6363import { countTokensOpenAIcountTokensOpenAIAsync, getTokenizerModel } from './tokenizers.js';
6464import { isMobile } from './RossAscends-mods.js';
6565import { saveLogprobsForActiveMessage } from './logprobs.js';
6666import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
@@ -671,14 +671,14 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
671671
672672 // Reserve budget for new chat message
673673 const newChat = selected_group ? oai_settings.new_group_chat_prompt : oai_settings.new_chat_prompt;
674674 const newChatMessage = newawait Message.createAsync('system', substituteParams(newChat), 'newMainChat');
675675 chatCompletion.reserveBudget(newChatMessage);
676676
677677 // Reserve budget for group nudge
678678 let groupNudgeMessage = null;
679679 const noGroupNudgeTypes = ['impersonate'];
680680 if (selected_group && prompts.has('groupNudge') && !noGroupNudgeTypes.includes(type)) {
681681 groupNudgeMessage = await Message.fromPromptfromPromptAsync(prompts.get('groupNudge'));
682682 chatCompletion.reserveBudget(groupNudgeMessage);
683683 }
684684
@@ -693,12 +693,12 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
693693 };
694694 const continuePrompt = new Prompt(promptObject);
695695 const preparedPrompt = promptManager.preparePrompt(continuePrompt);
696696 continueMessage = await Message.fromPromptfromPromptAsync(preparedPrompt);
697697 chatCompletion.reserveBudget(continueMessage);
698698 }
699699
700700 const lastChatPrompt = messages[messages.length - 1];
701701 const message = newawait Message.createAsync('user', oai_settings.send_if_empty, 'emptyUserMessageReplacement');
702702 if (lastChatPrompt && lastChatPrompt.role === 'assistant' && oai_settings.send_if_empty && chatCompletion.canAfford(message)) {
703703 chatCompletion.insert(message, 'chatHistory');
704704 }
@@ -715,11 +715,11 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
715715 // We do not want to mutate the prompt
716716 const prompt = new Prompt(chatPrompt);
717717 prompt.identifier = `chatHistory-${messages.length - index}`;
718718 const chatMessage = await Message.fromPromptfromPromptAsync(promptManager.preparePrompt(prompt));
719719
720720 if (promptManager.serviceSettings.names_behavior === character_names_behavior.COMPLETION && prompt.name) {
721721 const messageName = promptManager.isValidName(prompt.name) ? prompt.name : promptManager.sanitizeName(prompt.name);
722722 await chatMessage.setName(messageName);
723723 }
724724
725725 if (imageInlining && chatPrompt.image) {
@@ -729,9 +729,9 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
729729 if (canUseTools && Array.isArray(chatPrompt.invocations)) {
730730 /** @type {import('./tool-calling.js').ToolInvocation[]} */
731731 const invocations = chatPrompt.invocations;
732732 const toolCallMessage = newawait Message.createAsync(chatMessage.role, undefined, 'toolCall-' + chatMessage.identifier);
733733 const toolResultMessages = await Promise.all(invocations.slice().reverse().map((invocation) => new Message.createAsync('tool', invocation.result || '[No content]', invocation.id)));
734734 await toolCallMessage.setToolCalls(invocations);
735735 if (chatCompletion.canAffordAll([toolCallMessage, ...toolResultMessages])) {
736736 for (const resultMessage of toolResultMessages) {
737737 chatCompletion.insertAtStart(resultMessage, 'chatHistory');
@@ -748,7 +748,8 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
748748 if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {
749749 // 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
750750 if (chatPrompt.role === 'assistant') {
751751 const collectioncontinueMessage = new MessageCollection('continuePrefill', newawait Message.createAsync(chatMessage.role, substituteParams(oai_settings.assistant_prefill + '\n\n') + chatMessage.content, chatMessage.identifier));
752+ const collection = new MessageCollection('continuePrefill', continueMessage);
752753 chatCompletion.add(collection, -1);
753754 continue;
754755 }
@@ -787,18 +788,17 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
787788 * @param {ChatCompletion} chatCompletion - An instance of ChatCompletion class that will be populated with the prompts.
788789 * @param {Object[]} messageExamples - Array containing all message examples.
789790 */
790791async function populateDialogueExamples(prompts, chatCompletion, messageExamples) {
791792 if (!prompts.has('dialogueExamples')) {
792793 return;
793794 }
794795
795796 chatCompletion.add(new MessageCollection('dialogueExamples'), prompts.index('dialogueExamples'));
796797 if (Array.isArray(messageExamples) && messageExamples.length) {
797798 const newExampleChat = newawait Message.createAsync('system', substituteParams(oai_settings.new_example_chat_prompt), 'newChat');
798799 for (const dialogue of [...messageExamples].forEach((dialogue, dialogueIndex) => {
799800 letconst examplesAddeddialogueIndex = 0messageExamples.indexOf(dialogue);
800-
801+ const chatMessages = [];
801- if (chatCompletion.canAfford(newExampleChat)) chatCompletion.insert(newExampleChat, 'dialogueExamples');
802802
803803 for (let promptIndex = 0; promptIndex < dialogue.length; promptIndex++) {
804804 const prompt = dialogue[promptIndex];
@@ -806,19 +806,20 @@ function populateDialogueExamples(prompts, chatCompletion, messageExamples) {
806806 const content = prompt.content || '';
807807 const identifier = `dialogueExamples ${dialogueIndex}-${promptIndex}`;
808808
809809 const chatMessage = newawait Message.createAsync(role, content, identifier);
810810 await chatMessage.setName(prompt.name);
811- if (!chatCompletion.canAfford(chatMessage)) {
811+ chatMessages.push(chatMessage);
812- break;
813- }
814- chatCompletion.insert(chatMessage, 'dialogueExamples');
815- examplesAdded++;
816812 }
817813
818- if (0 === examplesAdded) {
814+ if (!chatCompletion.canAffordAll([newExampleChat, ...chatMessages])) {
819- chatCompletion.removeLastFrom('dialogueExamples');
815+ break;
820816 }
821- });
817+
818+ chatCompletion.insert(newExampleChat, 'dialogueExamples');
819+ for (const chatMessage of chatMessages) {
820+ chatCompletion.insert(chatMessage, 'dialogueExamples');
821+ }
822+ }
822823 }
823824}
824825
@@ -873,7 +874,7 @@ function getPromptRole(role) {
873874 */
874875async function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples }) {
875876 // Helper function for preparing a prompt, that already exists within the prompt collection, for completion
876877 const addToChatCompletion = async (source, target = null) => {
877878 // We need the prompts array to determine a position for the source.
878879 if (false === prompts.has(source)) return;
879880
@@ -891,30 +892,31 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
891892
892893 const index = target ? prompts.index(target) : prompts.index(source);
893894 const collection = new MessageCollection(source);
894- collection.add(Message.fromPrompt(prompt));
895+ const message = await Message.fromPromptAsync(prompt);
896+ collection.add(message);
895897 chatCompletion.add(collection, index);
896898 };
897899
898900 chatCompletion.reserveBudget(3); // every reply is primed with <|start|>assistant<|message|>
899901 // Character and world information
900902 await addToChatCompletion('worldInfoBefore');
901903 await addToChatCompletion('main');
902904 await addToChatCompletion('worldInfoAfter');
903905 await addToChatCompletion('charDescription');
904906 await addToChatCompletion('charPersonality');
905907 await addToChatCompletion('scenario');
906908 await addToChatCompletion('personaDescription');
907909
908910 // Collection of control prompts that will always be positioned last
909911 chatCompletion.setOverriddenPrompts(prompts.overriddenPrompts);
910912 const controlPrompts = new MessageCollection('controlPrompts');
911913
912914 const impersonateMessage = await Message.fromPromptfromPromptAsync(prompts.get('impersonate')) ?? null;
913915 if (type === 'impersonate') controlPrompts.add(impersonateMessage);
914916
915917 // Add quiet prompt to control prompts
916918 // This should always be last, even in control prompts. Add all further control prompts BEFORE this prompt
917919 const quietPromptMessage = await Message.fromPromptfromPromptAsync(prompts.get('quietPrompt')) ?? null;
918920 if (quietPromptMessage && quietPromptMessage.content) {
919921 if (isImageInliningSupported() && quietImage) {
920922 await quietPromptMessage.addImage(quietImage);
@@ -940,20 +942,23 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
940942 return acc;
941943 }, []);
942944
943945 for (const identifier of [...systemPrompts, ...userRelativePrompts].forEach(identifier => addToChatCompletion(identifier)); {
946+ await addToChatCompletion(identifier);
947+ }
944948
945949 // Add enhance definition instruction
946950 if (prompts.has('enhanceDefinitions')) await addToChatCompletion('enhanceDefinitions');
947951
948952 // Bias
949953 if (bias && bias.trim().length) await addToChatCompletion('bias');
950954
951955 // Tavern Extras - Summary
952956 if (prompts.has('summary')) {
953957 const summary = prompts.get('summary');
954958
955959 if (summary.position) {
956- chatCompletion.insert(Message.fromPrompt(summary), 'main', summary.position);
960+ const message = await Message.fromPromptAsync(summary);
961+ chatCompletion.insert(message, 'main', summary.position);
957962 }
958963 }
959964
@@ -962,7 +967,8 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
962967 const authorsNote = prompts.get('authorsNote');
963968
964969 if (authorsNote.position) {
965- chatCompletion.insert(Message.fromPrompt(authorsNote), 'main', authorsNote.position);
970+ const message = await Message.fromPromptAsync(authorsNote);
971+ chatCompletion.insert(message, 'main', authorsNote.position);
966972 }
967973 }
968974
@@ -971,7 +977,8 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
971977 const vectorsMemory = prompts.get('vectorsMemory');
972978
973979 if (vectorsMemory.position) {
974- chatCompletion.insert(Message.fromPrompt(vectorsMemory), 'main', vectorsMemory.position);
980+ const message = await Message.fromPromptAsync(vectorsMemory);
981+ chatCompletion.insert(message, 'main', vectorsMemory.position);
975982 }
976983 }
977984
@@ -980,7 +987,8 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
980987 const vectorsDataBank = prompts.get('vectorsDataBank');
981988
982989 if (vectorsDataBank.position) {
983- chatCompletion.insert(Message.fromPrompt(vectorsDataBank), 'main', vectorsDataBank.position);
990+ const message = await Message.fromPromptAsync(vectorsDataBank);
991+ chatCompletion.insert(message, 'main', vectorsDataBank.position);
984992 }
985993 }
986994
@@ -989,13 +997,15 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
989997 const smartContext = prompts.get('smartContext');
990998
991999 if (smartContext.position) {
992- chatCompletion.insert(Message.fromPrompt(smartContext), 'main', smartContext.position);
1000+ const message = await Message.fromPromptAsync(smartContext);
1001+ chatCompletion.insert(message, 'main', smartContext.position);
9931002 }
9941003 }
9951004
9961005 // Other relative extension prompts
9971006 for (const prompt of prompts.collection.filter(p => p.extension && p.position)) {
998- chatCompletion.insert(Message.fromPrompt(prompt), 'main', prompt.position);
1007+ const message = await Message.fromPromptAsync(prompt);
1008+ chatCompletion.insert(message, 'main', prompt.position);
9991009 }
10001010
10011011 // Pre-allocation of tokens for tool data
@@ -1003,7 +1013,7 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
10031013 const toolData = {};
10041014 await ToolManager.registerFunctionToolsOpenAI(toolData);
10051015 const toolMessage = [{ role: 'user', content: JSON.stringify(toolData) }];
10061016 const toolTokens = await tokenHandler.countcountAsync(toolMessage);
10071017 chatCompletion.reserveBudget(toolTokens);
10081018 }
10091019
@@ -1012,11 +1022,11 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
10121022
10131023 // Decide whether dialogue examples should always be added
10141024 if (power_user.pin_examples) {
10151025 await populateDialogueExamples(prompts, chatCompletion, messageExamples);
10161026 await populateChatHistory(messages, prompts, chatCompletion, type, cyclePrompt);
10171027 } else {
10181028 await populateChatHistory(messages, prompts, chatCompletion, type, cyclePrompt);
10191029 await populateDialogueExamples(prompts, chatCompletion, messageExamples);
10201030 }
10211031
10221032 chatCompletion.freeBudget(controlPrompts);
@@ -1281,7 +1291,7 @@ export async function prepareOpenAIMessages({
12811291 promptManager.setChatCompletion(chatCompletion);
12821292
12831293 if (oai_settings.squash_system_messages && dryRun == false) {
12841294 await chatCompletion.squashSystemMessages();
12851295 }
12861296
12871297 // All information is up-to-date, render.
@@ -2127,8 +2137,11 @@ async function calculateLogitBias() {
21272137}
21282138
21292139class TokenHandler {
2130- constructor(countTokenFn) {
2140+ /**
2131- this.countTokenFn = countTokenFn;
2141+ * @param {(messages: object[] | object, full?: boolean) => Promise<number>} countTokenAsyncFn Function to count tokens
2142+ */
2143+ constructor(countTokenAsyncFn) {
2144+ this.countTokenAsyncFn = countTokenAsyncFn;
21322145 this.counts = {
21332146 'start_chat': 0,
21342147 'prompt': 0,
@@ -2157,8 +2170,15 @@ class TokenHandler {
21572170 this.counts[type] -= value;
21582171 }
21592172
2160- count(messages, full, type) {
2173+ /**
2161- const token_count = this.countTokenFn(messages, full);
2174+ * Count tokens for a message or messages.
2175+ * @param {object|any[]} messages Messages to count tokens for
2176+ * @param {boolean} [full] Count full tokens
2177+ * @param {string} [type] Identifier for the token count
2178+ * @returns {Promise<number>} The token count
2179+ */
2180+ async countAsync(messages, full, type) {
2181+ const token_count = await this.countTokenAsyncFn(messages, full);
21622182 this.counts[type] += token_count;
21632183
21642184 return token_count;
@@ -2178,7 +2198,7 @@ class TokenHandler {
21782198}
21792199
21802200
21812201const tokenHandler = new TokenHandler(countTokensOpenAIcountTokensOpenAIAsync);
21822202
21832203// Thrown by ChatCompletion when a requested prompt couldn't be found.
21842204class IdentifierNotFoundError extends Error {
@@ -2228,6 +2248,7 @@ class Message {
22282248 * @param {string} role - The role of the entity creating the message.
22292249 * @param {string} content - The actual content of the message.
22302250 * @param {string} identifier - A unique identifier for the message.
2251+ * @private Don't use this constructor directly. Use createAsync instead.
22312252 */
22322253 constructor(role, content, identifier) {
22332254 this.identifier = identifier;
@@ -2239,18 +2260,32 @@ class Message {
22392260 this.role = 'system';
22402261 }
22412262
2242- if (typeof this.content === 'string' && this.content.length > 0) {
2263+ this.tokens = 0;
2243- this.tokens = tokenHandler.count({ role: this.role, content: this.content });
2264+ }
2244- } else {
2265+
2245- this.tokens = 0;
2266+ /**
2267+ * Create a new Message instance.
2268+ * @param {string} role
2269+ * @param {string} content
2270+ * @param {string} identifier
2271+ * @returns {Promise<Message>} Message instance
2272+ */
2273+ static async createAsync(role, content, identifier) {
2274+ const message = new Message(role, content, identifier);
2275+
2276+ if (typeof message.content === 'string' && message.content.length > 0) {
2277+ message.tokens = await tokenHandler.countAsync({ role: message.role, content: message.content });
22462278 }
2279+
2280+ return message;
22472281 }
22482282
22492283 /**
22502284 * Reconstruct the message from a tool invocation.
22512285 * @param {import('./tool-calling.js').ToolInvocation[]} invocations - The tool invocations to reconstruct the message from.
2286+ * @returns {Promise<void>}
22522287 */
22532288 async setToolCalls(invocations) {
22542289 this.tool_calls = invocations.map(i => ({
22552290 id: i.id,
22562291 type: 'function',
@@ -2259,14 +2294,24 @@ class Message {
22592294 name: i.name,
22602295 },
22612296 }));
22622297 this.tokens = await tokenHandler.countcountAsync({ role: this.role, tool_calls: JSON.stringify(this.tool_calls) });
22632298 }
22642299
2265- setName(name) {
2300+ /**
2301+ * Add a name to the message.
2302+ * @param {string} name Name to set for the message.
2303+ * @returns {Promise<void>}
2304+ */
2305+ async setName(name) {
22662306 this.name = name;
22672307 this.tokens = await tokenHandler.countcountAsync({ role: this.role, content: this.content, name: this.name });
22682308 }
22692309
2310+ /**
2311+ * Adds an image to the message.
2312+ * @param {string} image Image URL or Data URL.
2313+ * @returns {Promise<void>}
2314+ */
22702315 async addImage(image) {
22712316 const textContent = this.content;
22722317 const isDataUrl = isDataURL(image);
@@ -2356,13 +2401,13 @@ class Message {
23562401 }
23572402
23582403 /**
23592404 * Create a new Message instance from a prompt asynchronously.
23602405 * @static
23612406 * @param {Object} prompt - The prompt object.
23622407 * @returns {Promise<Message>} A new instance of Message.
23632408 */
23642409 static fromPromptfromPromptAsync(prompt) {
23652410 return new Message.createAsync(prompt.role, prompt.content, prompt.identifier);
23662411 }
23672412
23682413 /**
@@ -2488,8 +2533,9 @@ export class ChatCompletion {
24882533
24892534 /**
24902535 * Combines consecutive system messages into one if they have no name attached.
2536+ * @returns {Promise<void>}
24912537 */
24922538 async squashSystemMessages() {
24932539 const excludeList = ['newMainChat', 'newChat', 'groupNudge'];
24942540 this.messages.collection = this.messages.flatten();
24952541
@@ -2509,7 +2555,7 @@ export class ChatCompletion {
25092555 if (shouldSquash(message)) {
25102556 if (lastMessage && shouldSquash(lastMessage)) {
25112557 lastMessage.content += '\n' + message.content;
25122558 lastMessage.tokens = await tokenHandler.countcountAsync({ role: lastMessage.role, content: lastMessage.content });
25132559 }
25142560 else {
25152561 squashedMessages.push(message);