Last active
July 10, 2024 13:06
-
-
Save avimar/1649504125b87e913bf41147029800a8 to your computer and use it in GitHub Desktop.
LiberChat Token Count
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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