You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

107 lines
3.0 KiB

11 months ago
  1. const { getEncodingNameForModel, getEncoding } = require("js-tiktoken");
  2. /**
  3. * @class TokenManager
  4. *
  5. * @notice
  6. * We cannot do estimation of tokens here like we do in the collector
  7. * because we need to know the model to do it.
  8. * Other issues are we also do reverse tokenization here for the chat history during cannonballing.
  9. * So here we are stuck doing the actual tokenization and encoding until we figure out what to do with prompt overflows.
  10. */
  11. class TokenManager {
  12. static instance = null;
  13. static currentModel = null;
  14. constructor(model = "gpt-3.5-turbo") {
  15. if (TokenManager.instance && TokenManager.currentModel === model) {
  16. this.log("Returning existing instance for model:", model);
  17. return TokenManager.instance;
  18. }
  19. this.model = model;
  20. this.encoderName = this.#getEncodingFromModel(model);
  21. this.encoder = getEncoding(this.encoderName);
  22. TokenManager.instance = this;
  23. TokenManager.currentModel = model;
  24. this.log("Initialized new TokenManager instance for model:", model);
  25. return this;
  26. }
  27. log(text, ...args) {
  28. console.log(`\x1b[35m[TokenManager]\x1b[0m ${text}`, ...args);
  29. }
  30. #getEncodingFromModel(model) {
  31. try {
  32. return getEncodingNameForModel(model);
  33. } catch {
  34. return "cl100k_base";
  35. }
  36. }
  37. /**
  38. * Pass in an empty array of disallowedSpecials to handle all tokens as text and to be tokenized.
  39. * @param {string} input
  40. * @returns {number[]}
  41. */
  42. tokensFromString(input = "") {
  43. try {
  44. const tokens = this.encoder.encode(String(input), undefined, []);
  45. return tokens;
  46. } catch (e) {
  47. console.error(e);
  48. return [];
  49. }
  50. }
  51. /**
  52. * Converts an array of tokens back to a string.
  53. * @param {number[]} tokens
  54. * @returns {string}
  55. */
  56. bytesFromTokens(tokens = []) {
  57. const bytes = this.encoder.decode(tokens);
  58. return bytes;
  59. }
  60. /**
  61. * Counts the number of tokens in a string.
  62. * @param {string} input
  63. * @returns {number}
  64. */
  65. countFromString(input = "") {
  66. const tokens = this.tokensFromString(input);
  67. return tokens.length;
  68. }
  69. /**
  70. * Estimates the number of tokens in a string or array of strings.
  71. * @param {string | string[]} input
  72. * @returns {number}
  73. */
  74. statsFrom(input) {
  75. if (typeof input === "string") return this.countFromString(input);
  76. // What is going on here?
  77. // https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb Item 6.
  78. // The only option is to estimate. From repeated testing using the static values in the code we are always 2 off,
  79. // which means as of Nov 1, 2023 the additional factor on ln: 476 changed from 3 to 5.
  80. if (Array.isArray(input)) {
  81. const perMessageFactorTokens = input.length * 3;
  82. const tokensFromContent = input.reduce(
  83. (a, b) => a + this.countFromString(b.content),
  84. 0
  85. );
  86. const diffCoefficient = 5;
  87. return perMessageFactorTokens + tokensFromContent + diffCoefficient;
  88. }
  89. throw new Error("Not a supported tokenized format.");
  90. }
  91. }
  92. module.exports = {
  93. TokenManager,
  94. };