Last active
June 25, 2024 13:08
-
-
Save insilications/a3ce3add092df165e5521eb2be0e73be to your computer and use it in GitHub Desktop.
This file contains hidden or 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
// ==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(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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/