Skip to content

Instantly share code, notes, and snippets.

@insilications
Last active June 25, 2024 13:08
Show Gist options
  • Save insilications/a3ce3add092df165e5521eb2be0e73be to your computer and use it in GitHub Desktop.
Save insilications/a3ce3add092df165e5521eb2be0e73be to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name OpenAI GPT-4o and Anthropic Sonnet 3.5 Token Counter for LibreChat v4
// @version 1.4
// @description Automatically count tokens of chat content on LibreChat. Based on https://gist.github.com/avimar/1649504125b87e913bf41147029800a8
// @match http://localhost:3080/*
// @grant none
// @require https://raw.githubusercontent.com/insilications/tiktoken/master/js/mydist/bundle.js
// ==/UserScript==
(function () {
"use strict";
const PRICES = {
gpt_4o: {
input: 5,
output: 15,
}, // GPT-4o input/output price is $5.00/$15.00 / 1M tokens
sonnet_35: {
input: 3,
output: 15,
}, // Sonnet 3.5 input/output price is $3.00/$15.00 / 1M tokens
};
// 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 = "tokenStats2";
document.body.appendChild(popup);
const enc = js_tiktoken.getEncoding("o200k_base");
let tokenCounts = {
completions: 0,
prompts: 0,
promptInputBox: 0,
};
// Username used to differentiate between prompts (user input) and completions (LLM output)
// Enter your LibreChat username that is displayed in bold characters, above your prompt context, next to profile picture, in chat
const username = "Username";
function updateTokenCounts() {
const finalCompletions = document.querySelectorAll(".final-completion");
let newCounts = {
prompts: 0,
completions: 0,
};
finalCompletions.forEach((el) => {
const isUser = el.querySelector("div.select-none.font-semibold").textContent.trim().toLowerCase().includes(username.toLowerCase());
const selector = isUser ? "prompts" : "completions";
el.querySelectorAll(".markdown").forEach((markdownEl) => {
newCounts[selector] += enc.encode(markdownEl.textContent).length;
});
});
if (newCounts.completions !== tokenCounts.completions || newCounts.prompts !== tokenCounts.prompts) {
Object.assign(tokenCounts, newCounts);
updatePopup();
}
}
function updatePromptTokenCount() {
const promptBox = document.getElementById("prompt-textarea");
const newCount = promptBox ? enc.encode(promptBox.value).length : 0;
if (tokenCounts.promptInputBox !== newCount) {
tokenCounts.promptInputBox = newCount;
updatePopup();
}
}
function updatePopup() {
const totalPromptTokens = tokenCounts.prompts + tokenCounts.promptInputBox;
const totalTokens = tokenCounts.completions + totalPromptTokens;
const calculateCost = (model, tokens, type) => ((tokens / 1e6) * PRICES[model][type]).toFixed(3);
const result = `Total Tokens: ${totalTokens}\nPrompt Tokens: ${totalPromptTokens}\nCompletion Tokens: ${tokenCounts.completions}\n\nGPT-4o Prompts: $${calculateCost("gpt_4o", totalPromptTokens, "input")}\nGPT−4o Completions: $${calculateCost("gpt_4o", totalPromptTokens, "input")}\nGPT-4o Completions: $${calculateCost("gpt_4o", tokenCounts.completions, "output")}\nGPT-4o Total: $${(parseFloat(calculateCost("gpt_4o", totalPromptTokens, "input")) + parseFloat(calculateCost("gpt_4o", tokenCounts.completions, "output"))).toFixed(3)}\n\nSonnet 3.5 Prompts: $${calculateCost("sonnet_35", totalPromptTokens, "input")}\nSonnet 3.5 Completions: $${calculateCost("sonnet_35", totalPromptTokens, "input")}
Sonnet 3.5 Completions: $${calculateCost("sonnet_35", tokenCounts.completions, "output")}\nSonnet 3.5 Total: $${(parseFloat(calculateCost("sonnet_35", totalPromptTokens, "input")) + parseFloat(calculateCost("sonnet_35", tokenCounts.completions, "output"))).toFixed(3)}`;
if (popup.innerText !== result) {
popup.innerText = result;
}
}
const debouncedUpdateTokenCounts = debounce(updateTokenCounts, 1000);
const debouncedUpdatePromptTokenCount = debounce(updatePromptTokenCount, 50);
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
function observeDOMChanges() {
new MutationObserver((mutationsList) => {
if (mutationsList.length === 1 && mutationsList[0].target.id === "tokenStats2") return;
debouncedUpdateTokenCounts();
}).observe(document.body, {
childList: true,
subtree: true,
});
}
async function observePromptBox() {
const textarea = await waitForElement("#prompt-textarea");
textarea.addEventListener("input", debouncedUpdatePromptTokenCount);
}
async function waitForElement(selector) {
while (!document.querySelector(selector)) {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
return document.querySelector(selector);
}
updateTokenCounts();
observeDOMChanges();
observePromptBox();
})();
@insilications
Copy link
Author

My fork of js-tiktoken if you want to build the bundle yourself (follow the build instructions for tiktoken itself, then inside js folder run yarn run bundle) https://github.com/insilications/tiktoken/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment