Skip to content

Commit 330106e

Browse files
authored
1 parent b9eadb0 commit 330106e

File tree

1 file changed

+292
-0
lines changed

1 file changed

+292
-0
lines changed

token-usage.html

+292
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Token Usage Calculator</title>
7+
<style>
8+
* {
9+
box-sizing: border-box;
10+
}
11+
12+
body {
13+
font-family: Helvetica, Arial, sans-serif;
14+
margin: 0;
15+
padding: 20px;
16+
line-height: 1.6;
17+
color: #333;
18+
}
19+
20+
.container {
21+
max-width: 800px;
22+
margin: 0 auto;
23+
}
24+
25+
h1 {
26+
font-weight: 500;
27+
margin-bottom: 20px;
28+
}
29+
30+
h2, h3 {
31+
font-weight: 500;
32+
}
33+
34+
textarea {
35+
width: 100%;
36+
height: 300px;
37+
padding: 12px;
38+
border: 1px solid #ccc;
39+
border-radius: 4px;
40+
font-family: monospace;
41+
font-size: 16px;
42+
margin-bottom: 20px;
43+
resize: vertical;
44+
}
45+
46+
.results {
47+
margin-top: 20px;
48+
}
49+
50+
.model-group {
51+
margin-bottom: 25px;
52+
padding: 15px;
53+
background-color: #f5f5f5;
54+
border-radius: 4px;
55+
}
56+
57+
.model-group h3 {
58+
margin-top: 0;
59+
margin-bottom: 10px;
60+
border-bottom: 1px solid #ddd;
61+
padding-bottom: 5px;
62+
}
63+
64+
.totals {
65+
font-weight: bold;
66+
margin-top: 10px;
67+
}
68+
69+
.error {
70+
color: #d9534f;
71+
font-weight: bold;
72+
padding: 15px;
73+
background-color: #f5f5f5;
74+
border-radius: 4px;
75+
}
76+
77+
.empty-state {
78+
padding: 15px;
79+
background-color: #f5f5f5;
80+
border-radius: 4px;
81+
color: #666;
82+
}
83+
84+
ul {
85+
margin-top: 10px;
86+
padding-left: 20px;
87+
}
88+
89+
a {
90+
color: #4a90e2;
91+
text-decoration: none;
92+
}
93+
94+
a:hover {
95+
text-decoration: underline;
96+
}
97+
98+
code {
99+
background-color: #f1f1f1;
100+
padding: 2px 4px;
101+
border-radius: 3px;
102+
font-family: monospace;
103+
}
104+
</style>
105+
</head>
106+
<body>
107+
<div class="container">
108+
<h1>Token usage calculator</h1>
109+
110+
<p>This tool helps you analyze token usage from Claude and other LLM API calls. Paste the YAML output from <a href="https://llm.datasette.io/" target="_blank">LLM</a>'s <code>llm logs -su</code> to see token consumption across your calls.</p>
111+
112+
<p>Paste token usage data in YAML format below. This tool works best with output from the <code>llm logs -su</code> command:</p>
113+
114+
<textarea id="usageInput" placeholder="- model: anthropic/claude-3-7-sonnet-latest
115+
datetime: '2025-03-12T20:03:44'
116+
conversation: 01jp5z9g81cxf095mt7gkakmq5
117+
system: Write a paragraph of documentation...
118+
usage:
119+
input: 1435
120+
output: 132"></textarea>
121+
122+
<div class="results" id="results">
123+
<div class="empty-state">
124+
<p>Results will appear here after you paste token usage data.</p>
125+
</div>
126+
</div>
127+
</div>
128+
129+
<script type="module">
130+
// Parse the YAML-like format and extract input/output values
131+
function parseUsageData(text) {
132+
const entries = [];
133+
let currentEntry = null;
134+
135+
// Split by lines and process
136+
const lines = text.split('\n');
137+
138+
for (let i = 0; i < lines.length; i++) {
139+
const line = lines[i].trim();
140+
141+
// New entry starts with a dash
142+
if (line.startsWith('-')) {
143+
if (currentEntry) {
144+
// Only add entries that have both model and usage data
145+
if (currentEntry.model && (currentEntry.input > 0 || currentEntry.output > 0)) {
146+
entries.push(currentEntry);
147+
}
148+
}
149+
currentEntry = { model: '', input: 0, output: 0 };
150+
151+
// Extract model if on same line
152+
const modelMatch = line.match(/- model: (.+)/);
153+
if (modelMatch) {
154+
currentEntry.model = modelMatch[1];
155+
}
156+
}
157+
// Parse model if on separate line
158+
else if (line.startsWith('model:') && currentEntry) {
159+
currentEntry.model = line.replace('model:', '').trim();
160+
}
161+
// Parse input token count
162+
else if (line.startsWith('input:') && currentEntry) {
163+
const inputValue = parseInt(line.replace('input:', '').trim());
164+
if (!isNaN(inputValue)) {
165+
currentEntry.input = inputValue;
166+
}
167+
}
168+
// Parse output token count
169+
else if (line.startsWith('output:') && currentEntry) {
170+
const outputValue = parseInt(line.replace('output:', '').trim());
171+
if (!isNaN(outputValue)) {
172+
currentEntry.output = outputValue;
173+
}
174+
}
175+
}
176+
177+
// Add the last entry if exists and has valid data
178+
if (currentEntry && currentEntry.model && (currentEntry.input > 0 || currentEntry.output > 0)) {
179+
entries.push(currentEntry);
180+
}
181+
182+
return entries;
183+
}
184+
185+
// Calculate totals from parsed data, grouped by model
186+
function calculateTotals(entries) {
187+
// Group entries by model
188+
const modelGroups = {};
189+
190+
for (const entry of entries) {
191+
// Skip entries without a model name
192+
if (!entry.model) continue;
193+
194+
const model = entry.model;
195+
196+
if (!modelGroups[model]) {
197+
modelGroups[model] = {
198+
input: 0,
199+
output: 0,
200+
count: 0,
201+
entries: []
202+
};
203+
}
204+
205+
modelGroups[model].input += entry.input;
206+
modelGroups[model].output += entry.output;
207+
modelGroups[model].count += 1;
208+
modelGroups[model].entries.push(entry);
209+
}
210+
211+
return {
212+
modelGroups,
213+
entries: entries.length
214+
};
215+
}
216+
217+
// Display the results
218+
function displayResults(results, entries) {
219+
const resultsDiv = document.getElementById('results');
220+
221+
if (entries.length === 0) {
222+
resultsDiv.innerHTML = '<p class="error">No valid entries found. Please check your format.</p>';
223+
return;
224+
}
225+
226+
const modelCount = Object.keys(results.modelGroups).length;
227+
228+
if (modelCount === 0) {
229+
resultsDiv.innerHTML = '<p class="error">No models with valid token data found.</p>';
230+
return;
231+
}
232+
233+
let html = '<h2>Results</h2>';
234+
235+
// Display entries grouped by model
236+
html += `<p>Found ${entries.length} entries across ${modelCount} models:</p>`;
237+
238+
// Sort models alphabetically
239+
const sortedModels = Object.keys(results.modelGroups).sort();
240+
241+
for (const modelName of sortedModels) {
242+
const modelData = results.modelGroups[modelName];
243+
244+
html += `<div class="model-group">`;
245+
html += `<h3>${modelName} (${modelData.count} ${modelData.count === 1 ? 'entry' : 'entries'})</h3>`;
246+
247+
// Model totals only
248+
html += `<div class="totals">`;
249+
html += `<p>Total input tokens: ${modelData.input}</p>`;
250+
html += `<p>Total output tokens: ${modelData.output}</p>`;
251+
html += `</div>`;
252+
html += `</div>`;
253+
}
254+
255+
resultsDiv.innerHTML = html;
256+
}
257+
258+
// Main function to process the data
259+
function processUsageData() {
260+
const inputText = document.getElementById('usageInput').value;
261+
262+
if (!inputText.trim()) {
263+
document.getElementById('results').innerHTML = '<div class="empty-state"><p>Results will appear here after you paste token usage data.</p></div>';
264+
return;
265+
}
266+
267+
try {
268+
const entries = parseUsageData(inputText);
269+
const totals = calculateTotals(entries);
270+
displayResults(totals, entries);
271+
} catch (error) {
272+
document.getElementById('results').innerHTML = `<p class="error">Error processing data: ${error.message}</p>`;
273+
}
274+
}
275+
276+
// Set up event listener for textarea changes
277+
document.getElementById('usageInput').addEventListener('input', function() {
278+
// Debounce to avoid excessive calculations while typing
279+
clearTimeout(this.debounceTimer);
280+
this.debounceTimer = setTimeout(processUsageData, 300);
281+
});
282+
283+
// Process data on initial load if textarea has content
284+
window.addEventListener('load', function() {
285+
const textarea = document.getElementById('usageInput');
286+
if (textarea.value.trim()) {
287+
processUsageData();
288+
}
289+
});
290+
</script>
291+
</body>
292+
</html>

0 commit comments

Comments
 (0)