Skip to content

Instantly share code, notes, and snippets.

@avimar
Last active July 10, 2024 13:06
Show Gist options
  • Save avimar/1649504125b87e913bf41147029800a8 to your computer and use it in GitHub Desktop.
Save avimar/1649504125b87e913bf41147029800a8 to your computer and use it in GitHub Desktop.
LiberChat Token Count
//Via: Austin @ https://discord.com/channels/1086345563026489514/1086345563026489517/1239210064783474688
//Small updates via https://gist.github.com/insilications/a3ce3add092df165e5521eb2be0e73be for tokenizer, stats box.
//Edits: - Include prompt field, version 2
// - estimate for GPT 4o/sonnet & Opus at 2024-06-02 pricing
// ==UserScript==
// @name OpenAI Token Counter for LibreChat
// @namespace http://tampermonkey.net/
// @version 1.3
// @description Automatically count tokens of chat content on LibreChat
// @author ChatGPT 4
// @match https://YOUR DOMAIN*
// @grant none
// @require https://raw.githubusercontent.com/insilications/tiktoken/master/js/mydist/bundle.js
// ==/UserScript==
(function() {
'use strict';
const getLength = (tokens) => tokens && js_tiktoken.getEncoding("o200k_base").encode(tokens).length || 0;
const prices={ //input prices per 1M tokens
gpt_4o: 5
,claude_sonnet: 3
,claude_opus: 15
};
// Create and style popup element
const popup = document.createElement("div");
Object.assign(popup.style, {
position: "fixed",
right: "20px",
bottom: "10px",
backgroundColor: "#333",
color: "#fff",
padding: "10px",
borderRadius: "5px",
zIndex: "1001",
});
popup.id= 'tokenStats';
document.body.appendChild(popup);
// Debounce function to limit the rate of function calls
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
var tokenCountCompletions = 0;
var tokenCountPrompt = 0;
function updateTokenCountCompletions(mutations){
//if(mutations) console.log(mutations);
if (mutations && mutations.length === 1 && mutations[0].target.id === 'tokenStats') {
return; // only mutation is secondary token stats update, quit early
}
//console.log(observer);
console.log('calculating completions token count...');
const preWrapElements = document.querySelectorAll('.markdown');
//const codeElements = document.querySelectorAll('code');//pretty sure this is included in the markdown
let totalTokens = 0;
preWrapElements.forEach(element => {
const text = element.textContent;
const tokens = getLength(text);
totalTokens += tokens;
});
if(tokenCountCompletions!=totalTokens){
tokenCountCompletions=totalTokens;
writeTotalCount();
}
}
function updateTokenCountPrompt(){
console.log('updating prompt token count');
const promptBox = document.getElementById('prompt-textarea');
let promptBoxText = promptBox && promptBox.value;
if(promptBoxText) {
let promptBoxTokens = getLength(promptBoxText);
var totalTokens = promptBoxTokens;
}
else totalTokens = 0;
if(tokenCountPrompt!=totalTokens){
tokenCountPrompt=totalTokens;
writeTotalCount();
}
}
function writeTotalCount(){
const totalTokens = tokenCountCompletions+tokenCountPrompt;
const result = `Total Tokens: ${totalTokens}
GPT-4o/Sonnet: ${(totalTokens/1000000*prices.gpt_4o).toFixed(3)}
Claude-Opus: ${(totalTokens/1000000*prices.claude_opus).toFixed(3)}`;//no spaces which will be trunacted to avoid mutation cycles
if(!popup.innerText || popup.innerText!=result) {
console.log('updating stats text...');
//console.log(result==popup.innerText,[popup.innerText, result]);
popup.innerText=result;
}
}
const debouncedUpdateTokenCount = debounce(updateTokenCountCompletions, 100);
const debouncedUpdateTokenCountPrompt = debounce(updateTokenCountPrompt, 50);
function observeDOMChanges() {
const observer = new MutationObserver(debouncedUpdateTokenCount);
observer.observe(document.body, { childList: true, subtree: true });
}
async function observePromptBox() {
//console.log('waiting for prompt box');
const textarea = await waitForElement('#prompt-textarea');
console.log('attached to prompt box');
textarea.onchange= () => {//when the system clears it after submit, onkeyup isn't triggering, and it can be a long wait until we get a response, leading to double counting tokens.
debouncedUpdateTokenCountPrompt();
};
textarea.onkeyup = () => {
debouncedUpdateTokenCountPrompt();
};
}
async function waitForElement(selector) {
let element;
while (!element) {
element = document.querySelector(selector);
if (!element) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
return element;
}
updateTokenCountCompletions();
observeDOMChanges();
observePromptBox();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment