Use string byte length for token guesstimation (#5267) * Use string byte length for token guesstimation * Use Buffer.byteLength on backend * Preserve TextEncoder instance

e0ed67357ccc02bb3ae8f9b605a85354b44c91ec

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

Signed
2 files changed, +19 -6Ignore whitespace
public/scripts/tokenizers.js+5 -2
@@ -7,8 +7,9 @@ import { getStringHash } from './utils.js';
7import { kai_flags, kai_settings } from './kai-settings.js';7import { kai_flags, kai_settings } from './kai-settings.js';
8import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer, getTextGenModel } from './textgen-settings.js';8import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer, getTextGenModel } from './textgen-settings.js';
9import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer, openRouterModels } from './textgen-models.js';9import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer, openRouterModels } from './textgen-models.js';
10export { BYTES_PER_TOKEN as CHARACTERS_PER_TOKEN_RATIO };
1011
11export const CHARACTERS_PER_TOKEN_RATIO = 3.35;12export const BYTES_PER_TOKEN = 3.35;
12export const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown';13export const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown';
13export const TOKENIZER_SUPPORTED_KEY = 'tokenizationSupported';14export const TOKENIZER_SUPPORTED_KEY = 'tokenizationSupported';
1415
@@ -152,6 +153,7 @@ const TOKENIZER_URLS = {
152 },153 },
153};154};
154155
156const textEncoder = new TextEncoder();
155const objectStore = localforage.createInstance({ name: 'SillyTavern_ChatCompletions' });157const objectStore = localforage.createInstance({ name: 'SillyTavern_ChatCompletions' });
156158
157let tokenCache = {};159let tokenCache = {};
@@ -162,7 +164,8 @@ let tokenCache = {};
162 * @returns {number} Token count.164 * @returns {number} Token count.
163 */165 */
164export function guesstimate(str) {166export function guesstimate(str) {
165 return Math.ceil(str.length / CHARACTERS_PER_TOKEN_RATIO);167 const byteLength = textEncoder.encode(str).length;
168 return Math.ceil(byteLength / BYTES_PER_TOKEN);
166}169}
167170
168async function loadTokenCache() {171async function loadTokenCache() {
src/endpoints/tokenizers.js+14 -4
@@ -57,11 +57,21 @@ export const TEXT_COMPLETION_MODELS = [
57 'code-search-ada-code-001',57 'code-search-ada-code-001',
58];58];
5959
60const CHARS_PER_TOKEN = 3.35;60const BYTES_PER_TOKEN = 3.35;
61const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean');61const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean');
62const gunzip = promisify(zlib.gunzip);62const gunzip = promisify(zlib.gunzip);
6363
64/**64/**
65 * Guesstimates the token count for a string.
66 * @param {string} str String to tokenize.
67 * @returns {number} Token count.
68 */
69function guesstimate(str) {
70 const byteLength = Buffer.byteLength(str, 'utf8');
71 return Math.ceil(byteLength / BYTES_PER_TOKEN);
72}
73
74/**
65 * Gets a path to the tokenizer model. Downloads the model if it's a URL.75 * Gets a path to the tokenizer model. Downloads the model if it's a URL.
66 * @param {string} model Model URL or path76 * @param {string} model Model URL or path
67 * @param {string|undefined} fallbackModel Fallback model path77 * @param {string|undefined} fallbackModel Fallback model path
@@ -361,7 +371,7 @@ async function countSentencepieceTokens(tokenizer, text) {
361 if (!instance) {371 if (!instance) {
362 return {372 return {
363 ids: [],373 ids: [],
364 count: Math.ceil(text.length / CHARS_PER_TOKEN),374 count: guesstimate(text),
365 };375 };
366 }376 }
367377
@@ -540,7 +550,7 @@ export function countWebTokenizerTokens(tokenizer, messages) {
540550
541 // Fallback to strlen estimation551 // Fallback to strlen estimation
542 if (!tokenizer) {552 if (!tokenizer) {
543 return Math.ceil(convertedPrompt.length / CHARS_PER_TOKEN);553 return guesstimate(convertedPrompt);
544 }554 }
545555
546 const count = tokenizer.encode(convertedPrompt).length;556 const count = tokenizer.encode(convertedPrompt).length;
@@ -1021,7 +1031,7 @@ router.post('/openai/count', async function (req, res) {
1021 } catch (error) {1031 } catch (error) {
1022 console.error('An error counting tokens, using fallback estimation method', error);1032 console.error('An error counting tokens, using fallback estimation method', error);
1023 const jsonBody = JSON.stringify(req.body);1033 const jsonBody = JSON.stringify(req.body);
1024 const num_tokens = Math.ceil(jsonBody.length / CHARS_PER_TOKEN);1034 const num_tokens = guesstimate(jsonBody);
1025 res.send({ 'token_count': num_tokens });1035 res.send({ 'token_count': num_tokens });
1026 }1036 }
1027});1037});