Skip to content

Instantly share code, notes, and snippets.

@avimar
Last active June 6, 2024 09:50
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
//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://unpkg.com/gpt-tokenizer/dist/cl100k_base.js
// ==/UserScript==
(function() {
'use strict';
const prices={
gpt_4o: 5
,claude_sonnet: 3
,claude_opus: 15
};
const popup = document.createElement('div');
popup.style.position = 'fixed';
popup.style.right = '20px';
popup.style.bottom = '80px';
popup.style.backgroundColor = '#333';
popup.style.color = '#fff';
popup.style.padding = '10px';
popup.style.borderRadius = '5px';
popup.style.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 = GPTTokenizer_cl100k_base.encode(text);
totalTokens += tokens.length;
});
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 = GPTTokenizer_cl100k_base.encode(promptBoxText);
var totalTokens = promptBoxTokens.length;
}
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