Use string byte length for token guesstimation (#5267) * Use string byte length for token guesstimation * Use Buffer.byteLength on backend * Preserve TextEncoder instance
Signed| @@ -7,8 +7,9 @@ import { getStringHash } from './utils.js'; | |||
| 7 | import { kai_flags, kai_settings } from './kai-settings.js'; | 7 | import { kai_flags, kai_settings } from './kai-settings.js'; |
| 8 | import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer, getTextGenModel } from './textgen-settings.js'; | 8 | import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer, getTextGenModel } from './textgen-settings.js'; |
| 9 | import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer, openRouterModels } from './textgen-models.js'; | 9 | import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer, openRouterModels } from './textgen-models.js'; |
| 10 | export { BYTES_PER_TOKEN as CHARACTERS_PER_TOKEN_RATIO }; | ||
| 10 | 11 | ||
| 11 | export const CHARACTERS_PER_TOKEN_RATIO = 3.35; | 12 | export const BYTES_PER_TOKEN = 3.35; |
| 12 | export const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown'; | 13 | export const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown'; |
| 13 | export const TOKENIZER_SUPPORTED_KEY = 'tokenizationSupported'; | 14 | export const TOKENIZER_SUPPORTED_KEY = 'tokenizationSupported'; |
| 14 | 15 | ||
| @@ -152,6 +153,7 @@ const TOKENIZER_URLS = { | |||
| 152 | }, | 153 | }, |
| 153 | }; | 154 | }; |
| 154 | 155 | ||
| 156 | const textEncoder = new TextEncoder(); | ||
| 155 | const objectStore = localforage.createInstance({ name: 'SillyTavern_ChatCompletions' }); | 157 | const objectStore = localforage.createInstance({ name: 'SillyTavern_ChatCompletions' }); |
| 156 | 158 | ||
| 157 | let tokenCache = {}; | 159 | let tokenCache = {}; |
| @@ -162,7 +164,8 @@ let tokenCache = {}; | |||
| 162 | * @returns {number} Token count. | 164 | * @returns {number} Token count. |
| 163 | */ | 165 | */ |
| 164 | export function guesstimate(str) { | 166 | export 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 | } |
| 167 | 170 | ||
| 168 | async function loadTokenCache() { | 171 | async function loadTokenCache() { |
| @@ -57,11 +57,21 @@ export const TEXT_COMPLETION_MODELS = [ | |||
| 57 | 'code-search-ada-code-001', | 57 | 'code-search-ada-code-001', |
| 58 | ]; | 58 | ]; |
| 59 | 59 | ||
| 60 | const CHARS_PER_TOKEN = 3.35; | 60 | const BYTES_PER_TOKEN = 3.35; |
| 61 | const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean'); | 61 | const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean'); |
| 62 | const gunzip = promisify(zlib.gunzip); | 62 | const gunzip = promisify(zlib.gunzip); |
| 63 | 63 | ||
| 64 | /** | 64 | /** |
| 65 | * Guesstimates the token count for a string. | ||
| 66 | * @param {string} str String to tokenize. | ||
| 67 | * @returns {number} Token count. | ||
| 68 | */ | ||
| 69 | function 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 path | 76 | * @param {string} model Model URL or path |
| 67 | * @param {string|undefined} fallbackModel Fallback model path | 77 | * @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 | } |
| 367 | 377 | ||
| @@ -540,7 +550,7 @@ export function countWebTokenizerTokens(tokenizer, messages) { | |||
| 540 | 550 | ||
| 541 | // Fallback to strlen estimation | 551 | // 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 | } |
| 545 | 555 | ||
| 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 | }); |