|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<title>Am I overpaying for GPT-4?</title> |
|
<style> |
|
body { |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
margin: 40px; |
|
background-color: #f4f4f4; |
|
color: #333; |
|
} |
|
.container{max-width: 1200px; margin: 0 auto} |
|
h1, h4 { |
|
color: #333; |
|
} |
|
.upload_block { |
|
background-color: white; |
|
padding: 15px; |
|
border-radius: 8px; |
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1); |
|
margin-top: 15px; |
|
} |
|
input[type="file"] { |
|
border: 1px solid #ccc; |
|
display: inline-block; |
|
padding: 6px 12px; |
|
cursor: pointer; |
|
border-radius: 4px; |
|
transition: background-color 0.3s ease; |
|
} |
|
input[type="file"]:hover { |
|
background-color: #f0f0f0; |
|
} |
|
#loadingIndicator { |
|
display: none; |
|
margin-top: 15px; |
|
} |
|
#result { |
|
background-color: white; |
|
padding: 15px; |
|
margin-top: 15px; |
|
border-radius: 8px; |
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1); |
|
white-space: pre-wrap; |
|
display: none; |
|
position: relative; |
|
} |
|
ul { |
|
list-style-type: none; |
|
padding: 0; |
|
margin: 0; |
|
} |
|
ul li { |
|
padding: 8px; |
|
border-bottom: 1px solid #eee; |
|
} |
|
ul li:last-child { |
|
border-bottom: none; |
|
} |
|
a { |
|
color: #007bff; |
|
text-decoration: none; |
|
} |
|
a:hover { |
|
text-decoration: underline; |
|
} |
|
.color-red { |
|
color: #ff4136; |
|
} |
|
.color-green { |
|
color: #2ecc40; |
|
} |
|
.info-icon{position: absolute;right: 0;margin-right: 20px;color: gray; cursor: pointer} |
|
.details{display: none;} |
|
.loading span{ |
|
position: absolute; |
|
margin-top: 0.25em; |
|
margin-left: 0.5em; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<h1>Am I overpaying for ChatGPT?</h1> |
|
<h4>ChatGPT subscription is US$20 a month. Via <a href="https://platform.openai.com" target="_blank">the API</a>, GPT-4 Turbo costs US$0.03 per 1000 tokens. Check if you are overpaying or underpaying!</h4> |
|
<small>To export your history, go to <a href="https://chat.openai.com" target="_blank">ChatGPT</a> > Settings > Data Control > Export Data. This file is not sent to any server.</small> |
|
<div class="upload_block"> |
|
<p style="margin-top: 0">Upload your <code>conversations.json</code> file</p> |
|
<input type="file" id="fileInput" accept=".json"> |
|
|
|
</div> |
|
<div id="loadingIndicator" style="display: none;"> |
|
<p class="loading" style="color:gray"><img width=30 src="loading-buffering.gif" alt="Loading..."><span>Tokenizing messages locally with <code>transformers.js</code>...</span></p> |
|
</div> |
|
<pre id="result"></pre> |
|
<p><small>This tool runs locally on your machine and does not send your history to any server (you can verify it <a href="https://huggingface.co/spaces/multimodalart/am-i-overpaying-for-chatgpt/blob/main/index.html" target="_blank">here</a>). The number of tokens are calculated using <a href="https://huggingface.co/docs/transformers.js/en/index">transformers.js</a>. Image Generation and GPT4-V are taken into account.</small></p> |
|
</div> |
|
<script type="module"> |
|
function calculateVisionTokens(width, height) { |
|
const initialResizeWidth = width > 2048 || height > 2048 ? (width > height ? 2048 : Math.round(2048 * (width / height))) : width; |
|
const initialResizeHeight = width > 2048 || height > 2048 ? (width > height ? Math.round(2048 / (width / height)) : 2048) : height; |
|
|
|
const furtherResizeWidth = initialResizeWidth > 768 || initialResizeHeight > 768 |
|
? (initialResizeWidth < initialResizeHeight ? Math.min(768, initialResizeWidth) : Math.round(Math.min(768, initialResizeHeight) * (initialResizeWidth / initialResizeHeight))) |
|
: initialResizeWidth; |
|
|
|
const furtherResizeHeight = initialResizeWidth > 768 || initialResizeHeight > 768 |
|
? (initialResizeWidth < initialResizeHeight ? Math.round(Math.min(768, initialResizeWidth) / (initialResizeWidth / initialResizeHeight)) : Math.min(768, initialResizeHeight)) |
|
: initialResizeHeight; |
|
|
|
const verticalTiles = 1 + Math.ceil((furtherResizeHeight - 512) / (512 * (1 - 0))); |
|
const horizontalTiles = 1 + Math.ceil((furtherResizeWidth - 512) / (512 * (1 - 0))); |
|
const totalTiles = verticalTiles * horizontalTiles; |
|
|
|
const baseTokens = 85; |
|
const tileTokens = 170; |
|
const totalTokens = baseTokens + totalTiles * tileTokens; |
|
|
|
return totalTokens; |
|
} |
|
function calculateBranches(mapping) { |
|
const branches = []; |
|
|
|
|
|
function buildBranch(nodeId, currentBranch) { |
|
const node = mapping[nodeId]; |
|
if (!node) return; |
|
const updatedBranch = [...currentBranch, node]; |
|
if (node.children.length === 0) { |
|
branches.push(updatedBranch); |
|
} else { |
|
node.children.forEach(childId => { |
|
buildBranch(childId, updatedBranch); |
|
}); |
|
} |
|
} |
|
|
|
|
|
const rootNodes = Object.values(mapping).filter(node => !node.parent); |
|
|
|
|
|
rootNodes.forEach(rootNode => buildBranch(rootNode.id, [])); |
|
|
|
return branches; |
|
} |
|
|
|
import { AutoTokenizer } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.15.1'; |
|
document.addEventListener("DOMContentLoaded", async (event) => { |
|
const tokenizer = await AutoTokenizer.from_pretrained('Xenova/gpt-4'); |
|
const fileInput = document.getElementById('fileInput'); |
|
const resultElement = document.getElementById('result'); |
|
const loadingIndicator = document.getElementById('loadingIndicator'); |
|
|
|
const sevenMonthsAgo = new Date(); |
|
sevenMonthsAgo.setMonth(sevenMonthsAgo.getMonth() - 7); |
|
const currentDate = new Date(); |
|
const currentMonthYear = `${currentDate.getMonth() + 1}/${currentDate.getFullYear()}`; |
|
fileInput.addEventListener('change', function(event) { |
|
const file = event.target.files[0]; |
|
const reader = new FileReader(); |
|
|
|
loadingIndicator.style.display = 'block'; |
|
|
|
reader.onload = async function(e) { |
|
if (!tokenizer) { |
|
console.error('Tokenizer is not initialized.'); |
|
loadingIndicator.style.display = 'none'; |
|
return; |
|
} |
|
|
|
const jsonData = JSON.parse(e.target.result); |
|
const monthlyData = {}; |
|
let totalSavings = 0; |
|
|
|
jsonData.forEach(conversation => { |
|
const createTime = new Date(conversation.create_time * 1000); |
|
if (createTime >= sevenMonthsAgo) { |
|
const monthYear = `${createTime.getMonth() + 1}/${createTime.getFullYear()}`; |
|
|
|
if (!monthlyData[monthYear]) { |
|
monthlyData[monthYear] = { |
|
gpt4Tokens: 0, |
|
gpt4InputTokens: 0, |
|
gpt4VisionTokens: 0, |
|
gpt4OutputTokens: 0, |
|
gpt3Tokens: 0, |
|
gpt3InputTokens: 0, |
|
gpt3OutputTokens: 0, |
|
gpt4Cost: 0, |
|
gpt3Cost: 0, |
|
images: 0, |
|
rectangleImages: 0, |
|
imagesCost: 0, |
|
}; |
|
} |
|
|
|
|
|
let inputTokens = 0; |
|
let visionTokens = 0; |
|
let outputTokens = 0; |
|
let images = 0; |
|
let rectangleImages = 0; |
|
let currentModel = "gpt-4"; |
|
|
|
|
|
const branches = calculateBranches(conversation.mapping); |
|
branches.forEach(branch => { |
|
let conversationHistory = []; |
|
branch.forEach(node => { |
|
if (node.message && node.message.content && node.message.content.parts) { |
|
const role = node.message.author.role; |
|
const content = node.message.content.parts.join(' ').replace(/\[object Object\]/g, ''); |
|
const currentMessage = {"role": role, "content": content}; |
|
conversationHistory.push(currentMessage); |
|
|
|
if(role == "user"){ |
|
const input_ids = tokenizer.apply_chat_template(conversationHistory, { tokenize: true, return_tensor: false }); |
|
inputTokens += input_ids.length; |
|
|
|
if(node.message.content.content_type == "multimodal_text"){ |
|
for (const image of node.message.content.parts){ |
|
if(image.width && image.height){ |
|
visionTokens += calculateVisionTokens(image.width, image.height) |
|
} |
|
} |
|
} |
|
|
|
}else if(role == "assistant"){ |
|
const output_ids = tokenizer.apply_chat_template([currentMessage], { tokenize: true, return_tensor: false }); |
|
outputTokens += output_ids.length; |
|
|
|
}else if(role == "tool" && node.message.author.name == "dalle.text2im"){ |
|
if(node.message.content.content_type == "multimodal_text"){ |
|
for (const image of node.message.content.parts){ |
|
if(image.width && image.height){ |
|
if(image.width / image.height == 1){ |
|
images+=1; |
|
}else{ |
|
rectangleImages+= 1; |
|
} |
|
} |
|
} |
|
} |
|
|
|
}else{ |
|
return; |
|
} |
|
|
|
if (node.message.metadata && node.message.metadata.model_slug) { |
|
if (node.message.metadata.model_slug.includes('gpt-4')) { |
|
currentModel = "gpt-4"; |
|
}else{ |
|
currentModel = "gpt-3"; |
|
} |
|
} |
|
} |
|
}) |
|
}); |
|
if (currentModel == 'gpt-4') { |
|
const inputTokenCost = inputTokens * (10 / 1000000); |
|
const outputTokenCost = outputTokens * (30 / 1000000); |
|
monthlyData[monthYear].gpt4InputTokens += inputTokens; |
|
monthlyData[monthYear].gpt4OutputTokens += outputTokens; |
|
monthlyData[monthYear].gpt4Tokens += inputTokens + outputTokens; |
|
monthlyData[monthYear].gpt4Cost += inputTokenCost + outputTokenCost; |
|
}else{ |
|
const inputTokenCost = inputTokens * (0.5 / 1000000); |
|
const outputTokenCost = outputTokens * (1.5 / 1000000); |
|
monthlyData[monthYear].gpt3InputTokens += inputTokens; |
|
monthlyData[monthYear].gpt3OutputTokens += outputTokens; |
|
monthlyData[monthYear].gpt3Tokens += inputTokens + outputTokens; |
|
monthlyData[monthYear].gpt3Cost += inputTokenCost + outputTokenCost; |
|
} |
|
const imagesCost = images * 0.04; |
|
const RectanglesCost = rectangleImages * 0.08; |
|
const visionCost = visionTokens * 0.00001; |
|
monthlyData[monthYear].gpt4VisionTokens += visionTokens; |
|
monthlyData[monthYear].gpt4Cost += visionCost; |
|
monthlyData[monthYear].images += images; |
|
monthlyData[monthYear].rectangleImages += rectangleImages; |
|
monthlyData[monthYear].imagesCost += imagesCost + RectanglesCost; |
|
} |
|
}); |
|
|
|
let resultText = '<div class="info-icon">more details ⓘ</div><ul>'; |
|
|
|
Object.entries(monthlyData).forEach(([monthYear, data]) => { |
|
let totalCost = 0; |
|
if(data.gpt4Cost != 0 || data.imagesCost != 0){ |
|
totalCost = data.gpt4Cost + data.gpt3Cost + data.imagesCost; |
|
} |
|
|
|
if(currentMonthYear == monthYear){ |
|
resultText += `<li>${monthYear}: <b>Total API cost: $${totalCost.toFixed(2)}</b> (<i>current month</i>)`; |
|
}else if(totalCost > 0){ |
|
totalSavings += 20 - totalCost; |
|
resultText += `<li>${monthYear}: <b>Total API cost: $${totalCost.toFixed(2)}</b>`; |
|
resultText += (totalCost <= 20) ? |
|
`. <span class="color-red">You overpaid ChatGPT by <b>$${(20 - totalCost).toFixed(2)}</b> this month.</span>` : |
|
`. <span class="color-green">You underpaid ChatGPT by <b>$${(totalCost - 20).toFixed(2)}</b> this month.</span>`; |
|
}else{ |
|
resultText += `<li>${monthYear}: <small>we assume no subscription this month due to no GPT-4 usage</small>`; |
|
} |
|
resultText += `<br><span class="details" style="color:gray">gpt4InputTokens: ${data.gpt4InputTokens}, gpt4OutputTokens: ${data.gpt4OutputTokens}, gp4VisionTokens: ${data.gpt4VisionTokens}, gpt3InputTokens ${data.gpt3InputTokens}, gpt3OutputTokens ${data.gpt3OutputTokens}, DALL·E 3 Square Images: ${data.images}, DALL·E 3 Rectangular Images: ${data.rectangleImages}`; |
|
resultText += '</li>'; |
|
|
|
}); |
|
resultText += '</ul>'; |
|
|
|
const summaryText = totalSavings >= 0 ? |
|
`<span class="color-red">You are overpaying ChatGPT</span>. In the last 6 months, you could have saved US$${totalSavings.toFixed(2)} by using the API.` : |
|
`<span class="color-green">You are underpaying ChatGPT</span>. In the last 6 months, you saved US$${Math.abs(totalSavings).toFixed(2)} by being subscribed to ChaGPT Plus.`; |
|
|
|
const extraInfo = totalSavings > 0 ? `<div>You can use your <a href="https://platform.openai.com" target="_blank">GPT-4 Turbo API</a> in a ChatGPT-like UI with open source tools like <a href="https://github.com/huggingface/chat-ui/issues/253" target="_blank">🤗 ChatUI</a>, <a href="https://github.com/ztjhz/BetterChatGPT" target="_blank">BetterChatGPT</a>, <a href="https://github.com/deiucanta/chatpad" target="_blank">ChatPad</a><br><br>You may also use state of the art open source models for free with <a href="https://huggingface.co/chat/" target="_blank">HuggingChat</a></div>` : `` |
|
resultElement.innerHTML = resultText + `<h2>${summaryText}</h2> ${extraInfo}`; |
|
resultElement.style.display = 'block' |
|
loadingIndicator.style.display = 'none'; |
|
}; |
|
|
|
reader.readAsText(file); |
|
}); |
|
|
|
document.body.addEventListener('click', function(e) { |
|
if(e.target.classList.contains('info-icon')) { |
|
if (e.target.textContent.includes('more details')) { |
|
e.target.textContent = 'less details ⓘ'; |
|
|
|
document.querySelectorAll('.details').forEach(details => { |
|
details.style.display = 'block'; |
|
}); |
|
} else { |
|
e.target.textContent = 'more details ⓘ'; |
|
|
|
document.querySelectorAll('.details').forEach(details => { |
|
details.style.display = 'none'; |
|
}); |
|
} |
|
} |
|
}); |
|
}); |
|
</script> |
|
</body> |
|
</html> |