Model: gemini-2.5-pro-preview-05-06
-- none --
0e887c65afc97781a8565b8d2cb71d5ea015ce0225d3aa3656bdc6ae113430b4
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LLM pricing calculator</title>
<link rel="icon" type="image/png" href="favicon.png">
<style>
* {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.calculator, .presets, .disclaimer {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
@media (min-width: 768px) {
.flex-container {
display: flex;
gap: 20px;
}
.calculator {
flex: 1;
}
.presets {
flex: 2;
}
}
h1, h2 {
font-size: 24px;
color: #333;
margin-bottom: 20px;
text-align: center;
}
.input-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
color: #666;
}
input[type="number"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.result {
margin-top: 20px;
padding: 15px;
background-color: #e9f7ef;
border-radius: 4px;
}
.result p {
margin: 5px 0;
font-size: 18px;
color: #2ecc71;
}
.preset-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.preset-table th {
background-color: #f2f2f2;
padding: 10px;
text-align: left;
position: relative;
}
.preset-table th button {
background: none;
border: none;
font-weight: bold;
font-size: inherit;
text-align: left;
padding: 0;
margin: 0;
width: 100%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
}
.preset-table th button:hover,
.preset-table th button:focus {
background-color: #e0e0e0;
outline: 2px solid #0066cc;
}
.sort-icon {
margin-left: 5px;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.preset-table td {
padding: 8px 10px;
border-bottom: 1px solid #ddd;
}
.preset-table tr:hover {
background-color: #f5f5f5;
}
.model-name-btn {
background-color: #e1f0ff;
border: 1px solid #d0e5fa;
border-radius: 4px;
font-size: 14px;
color: #333;
cursor: pointer;
text-align: left;
padding: 5px 8px;
font-family: inherit;
width: 100%;
text-decoration: none;
transition: background-color 0.2s, border-color 0.2s;
}
.model-name-btn:hover,
.model-name-btn:focus {
background-color: #96bce0;
border-color: #6a9ccf;
outline: 2px solid #0066cc;
}
.disclaimer {
font-size: 14px;
color: #777;
text-align: center;
font-style: italic;
}
</style>
<script defer data-domain="llm-prices.com" src="https://plausible.io/js/script.js"></script>
</head>
<body>
<div class="container">
<div class="flex-container">
<div class="calculator">
<h1>LLM pricing calculator</h1>
<div class="input-group">
<label for="inputTokens">Number of input tokens (aka prompt tokens):</label>
<input type="number" id="inputTokens" min="0" onkeyup="calculateCost()" onchange="calculateCost()">
</div>
<div class="input-group">
<label for="outputTokens">Number of output tokens (aka completion tokens):</label>
<input type="number" id="outputTokens" min="0" onkeyup="calculateCost()" onchange="calculateCost()">
</div>
<div class="input-group">
<label for="inputCost">Cost per million input tokens ($):</label>
<input type="number" id="inputCost" min="0" step="0.000001" onkeyup="calculateCost()" onchange="calculateCost()">
</div>
<div class="input-group">
<label for="outputCost">Cost per million output tokens ($):</label>
<input type="number" id="outputCost" min="0" step="0.000001" onkeyup="calculateCost()" onchange="calculateCost()">
</div>
<div class="result">
<p>Total cost: $<span id="costDollars">0.000000</span></p>
<p>Total cost: <span id="costCents">0</span> cents</p>
</div>
</div>
<div class="presets">
<h2>Model prices (per million tokens)</h2>
<div id="presetTable">
<table class="preset-table">
<thead>
<tr>
<th aria-sort="none">
<button type="button" id="sortByName">
Model
<span class="sort-icon" aria-hidden="true"></span>
</button>
</th>
<th aria-sort="ascending">
<button type="button" id="sortByInput">
Input cost
<span class="sort-icon" aria-hidden="true">↑</span>
</button>
</th>
<th aria-sort="none">
<button type="button" id="sortByOutput">
Output cost
<span class="sort-icon" aria-hidden="true"></span>
</button>
</th>
</tr>
</thead>
<tbody id="presetTableBody">
<!-- Table rows will be populated by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
<div class="disclaimer">Prices may have changed since this tool was last updated.</div>
</div>
<script>
// Prices are $ per 1M tokens
const presets = {
'gemini-2.5-pro-preview-03-25': { name: 'Gemini 2.5 Pro Preview ≤200k', input: 1.25, output: 10.00 },
'gemini-2.5-pro-preview-03-25-200k': { name: 'Gemini 2.5 Pro Preview >200k', input: 2.50, output: 15.00 },
'gemini-2.0-flash-lite': { name: 'Gemini 2.0 Flash Lite', input: 0.075, output: 0.30 },
'gemini-2.0-flash': { name: 'Gemini 2.0 Flash', input: 0.10, output: 0.40 },
'gemini-1.5-flash': { name: 'Gemini 1.5 Flash ≤128k', input: 0.075, output: 0.30 },
'gemini-1.5-flash-128k': { name: 'Gemini 1.5 Flash >128k', input: 0.15, output: 0.60 },
'gemini-1.5-flash-8b': { name: 'Gemini 1.5 Flash-8B ≤128k', input: 0.0375, output: 0.15 },
'gemini-1.5-flash-8b-128k': { name: 'Gemini 1.5 Flash-8B >128k', input: 0.075, output: 0.30 },
'gemini-1.5-pro': { name: 'Gemini 1.5 Pro ≤128k', input: 1.25, output: 5.00 },
'gemini-1.5-pro-128k': { name: 'Gemini 1.5 Pro >128k', input: 2.50, output: 10.00 },
'claude-3.7-sonnet': { name: 'Claude 3.7 Sonnet', input: 3.00, output: 15.00 },
'claude-3.5-sonnet': { name: 'Claude 3.5 Sonnet', input: 3.00, output: 15.00 },
'claude-3-opus': { name: 'Claude 3 Opus', input: 15.00, output: 75.00 },
'claude-3-haiku': { name: 'Claude 3 Haiku', input: 0.25, output: 1.25 },
'claude-3.5-haiku': { name: 'Claude 3.5 Haiku', input: 0.80, output: 4.00 },
'gpt-4.5': { name: 'GPT-4.5', input: 75.00, output: 150.00 },
'gpt-4o': { name: 'GPT-4o', input: 2.50, output: 10.00 },
'gpt-4o-mini': { name: 'GPT-4o Mini', input: 0.150, output: 0.600 },
'chatgpt-4o-latest': { name: 'ChatGPT 4o Latest', input: 5.00, output: 15.00 },
'o1-preview': { name: 'o1 and o1-preview', input: 15.00, output: 60.00 },
'o1-pro': { name: 'o1 Pro', input: 150.00, output: 600.00 },
'o1-mini': { name: 'o1-mini', input: 1.10, output: 4.40 },
'o3-mini': { name: 'o3-mini', input: 1.10, output: 4.40 },
'amazon-nova-micro': { name: 'Amazon Nova Micro', input: 0.035, output: 0.14 },
'amazon-nova-lite': { name: 'Amazon Nova Lite', input: 0.06, output: 0.24 },
'amazon-nova-pro': { name: 'Amazon Nova Pro', input: 0.8, output: 3.2 },
'deepseek-chat': {
name: 'DeepSeek Chat',
getPrice: () => {
const cutoffDate = new Date('2025-02-08T16:00:00Z');
const currentDate = new Date();
if (currentDate < cutoffDate) {
return { input: 0.14, output: 0.28 };
} else {
return { input: 0.27, output: 1.10 };
}
}
},
'deepseek-reasoner': { name: 'DeepSeek Reasoner', input: 0.55, output: 2.19 },
'pixtral-12b': { name: 'Pixtral 12B', input: 0.15, output: 0.15 },
'mistral-small-latest': { name: 'Mistral Small 3.1', input: 0.1, output: 0.3 },
'mistral-medium-2505': { name: 'Mistral Medium 3', input: 0.4, output: 2 },
'mistral-nemo': { name: 'Mistral NeMo', input: 0.15, output: 0.15 },
'open-mistral-7b': { name: 'Mistral 7B', input: 0.25, output: 0.25 },
'open-mixtral-8x7b': { name: 'Mixtral 8x7B', input: 0.7, output: 0.7 },
'open-mixtral-8x22b': { name: 'Mixtral 8x22B', input: 2, output: 6 },
'mistral-large-latest': { name: 'Mistral Large 24.11', input: 2, output: 6 },
'pixtral-large-latest': { name: 'Pixtral Large', input: 2, output: 6 },
'mistral-saba-latest': { name: 'Mistral Saba', input: 0.2, output: 0.6 },
'codestral-latest': { name: 'Codestral', input: 0.3, output: 0.9 },
'ministral-8b-latest': { name: 'Ministral 8B 24.10', input: 0.1, output: 0.1 },
'ministral-3b-latest': { name: 'Ministral 3B 24.10', input: 0.04, output: 0.04 },
'grok-3-beta': { name: 'Grok 3 Beta', input: 3.00, output: 15.00 },
'grok-3-mini-beta': { name: 'Grok 3 Mini Beta', input: 0.30, output: 0.50 },
'gpt-4.1': { name: 'GPT 4.1', input: 2.00, output: 8.00 },
'gpt-4.1-mini': { name: 'GPT 4.1 Mini', input: 0.40, output: 1.60 },
'gpt-4.1-nano': { name: 'GPT 4.1 Nano', input: 0.10, output: 0.40 },
'o3': { name: 'o3', input: 10, output: 40 },
'o4-mini': { name: 'o4-mini', input: 1.1, output: 4.4 },
'gemini-2.5-flash': { name: 'Gemini 2.5 Flash', input: 0.15, output: 0.60 },
'gemini-2.5-flash-thinking': { name: 'Gemini 2.5 Flash Thinking', input: 0.15, output: 3.50 },
};
function calculateCost() {
const inputTokens = parseFloat(document.getElementById('inputTokens').value) || 0;
const outputTokens = parseFloat(document.getElementById('outputTokens').value) || 0;
const inputCost = parseFloat(document.getElementById('inputCost').value) || 0;
const outputCost = parseFloat(document.getElementById('outputCost').value) || 0;
const totalCost = (inputTokens * inputCost / 1000000) + (outputTokens * outputCost / 1000000);
document.getElementById('costDollars').textContent = totalCost.toFixed(6);
document.getElementById('costCents').textContent = trimZeros((totalCost * 100).toFixed(4));
}
function trimZeros(num) {
return num.replace(/\.?0+$/, '');
}
function setPreset(model) {
const preset = presets[model];
let prices;
if (preset.getPrice) {
prices = preset.getPrice();
} else {
prices = { input: preset.input, output: preset.output };
}
document.getElementById('inputCost').value = prices.input;
document.getElementById('outputCost').value = prices.output;
calculateCost();
}
function formatPrice(price) {
// For whole numbers (like $5)
if (Number.isInteger(price)) {
return `$${price}`;
}
// Get the number as a string and analyze
const priceStr = price.toString();
// If the number has more than 2 decimal places and ends with non-zero digits,
// preserve the exact representation to avoid loss of precision
if (priceStr.split('.')[1]?.length > 2 && !priceStr.endsWith('0')) {
return `$${priceStr}`;
}
// For all other decimal values (including 0.2, 0.15, etc.), ensure exactly 2 decimal places
return `$${price.toFixed(2)}`;
}
// Create presets table
function createPresetsTable() {
const presetTableBody = document.getElementById('presetTableBody');
const presetList = [];
// Convert presets object to array for sorting
for (const [key, preset] of Object.entries(presets)) {
let prices;
if (preset.getPrice) {
prices = preset.getPrice();
} else {
prices = { input: preset.input, output: preset.output };
}
presetList.push({
key: key,
name: preset.name,
input: prices.input,
output: prices.output
});
}
// Initial sort by input price (cheapest first)
presetList.sort((a, b) => a.input - b.input);
// Populate table
renderTable(presetList);
// Add event listeners to sort buttons
document.getElementById('sortByName').addEventListener('click', () => {
sortTable('name');
});
document.getElementById('sortByInput').addEventListener('click', () => {
sortTable('input');
});
document.getElementById('sortByOutput').addEventListener('click', () => {
sortTable('output');
});
}
// Render the table with the given data
function renderTable(data) {
const presetTableBody = document.getElementById('presetTableBody');
presetTableBody.innerHTML = '';
data.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
<td><button class="model-name-btn" onclick="setPreset('${item.key}')">${item.name}</button></td>
<td>${formatPrice(item.input)}</td>
<td>${formatPrice(item.output)}</td>
`;
presetTableBody.appendChild(row);
});
}
// Sort table by the given column
function sortTable(column) {
const sortColumns = {
'name': 0,
'input': 1,
'output': 2
};
const thElements = document.querySelectorAll('.preset-table th');
const columnTh = thElements[sortColumns[column]];
// Get the current sort direction for this column
const currentSort = columnTh.getAttribute('aria-sort');
let newSort;
// Toggle or set sort direction
if (currentSort === 'ascending') {
newSort = 'descending';
} else if (currentSort === 'descending') {
newSort = 'ascending';
} else {
// If not currently sorted by this column
newSort = (column === 'name') ? 'ascending' : 'ascending';
}
// Reset all sort indicators
thElements.forEach(th => {
th.setAttribute('aria-sort', 'none');
th.querySelector('.sort-icon').textContent = '';
});
// Set new sort state
columnTh.setAttribute('aria-sort', newSort);
columnTh.querySelector('.sort-icon').textContent = (newSort === 'ascending') ? '↑' : '↓';
// Convert presets object to array for sorting
const presetList = [];
for (const [key, preset] of Object.entries(presets)) {
let prices;
if (preset.getPrice) {
prices = preset.getPrice();
} else {
prices = { input: preset.input, output: preset.output };
}
presetList.push({
key: key,
name: preset.name,
input: prices.input,
output: prices.output
});
}
// Sort the data
presetList.sort((a, b) => {
if (column === 'name') {
return (newSort === 'ascending')
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name);
} else if (column === 'input') {
return (newSort === 'ascending')
? a.input - b.input
: b.input - a.input;
} else if (column === 'output') {
return (newSort === 'ascending')
? a.output - b.output
: b.output - a.output;
}
return 0;
});
// Render the sorted table
renderTable(presetList);
}
function handlePaste(event) {
const text = (event.clipboardData || window.clipboardData).getData('text');
const comboMatch = text.match(/([\d,]+)\s*input[, ]+([\d,]+)\s*output/i);
if (comboMatch) {
event.preventDefault();
document.getElementById('inputTokens').value = comboMatch[1].replace(/,/g, '');
document.getElementById('outputTokens').value = comboMatch[2].replace(/,/g, '');
calculateCost();
return;
}
const numMatch = text.match(/^\s*([\d,]+)\s*$/);
if (numMatch) {
event.preventDefault();
event.target.value = numMatch[1].replace(/,/g, '');
calculateCost();
}
}
document.getElementById('inputTokens').addEventListener('paste', handlePaste);
// Initialize the calculator and table
createPresetsTable();
calculateCost();
</script>
</body>
</html>
modify this code so that the state of the page is reflected in the fragmenth hash URL - I want to capture the values filling out the form fields and also the current sort order of the table. These should be respected when the page first loads too. Update them using replaceHistory, no need to enable the back button.
Okay, I'll modify your code to reflect the form field values and table sort order in the URL fragment hash. This state will be applied on page load, and the hash will be updated using history.replaceState
as changes occur.
Here's the updated code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LLM pricing calculator</title>
<link rel="icon" type="image/png" href="favicon.png">
<style>
* {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.calculator, .presets, .disclaimer {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
@media (min-width: 768px) {
.flex-container {
display: flex;
gap: 20px;
}
.calculator {
flex: 1;
}
.presets {
flex: 2;
}
}
h1, h2 {
font-size: 24px;
color: #333;
margin-bottom: 20px;
text-align: center;
}
.input-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
color: #666;
}
input[type="number"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.result {
margin-top: 20px;
padding: 15px;
background-color: #e9f7ef;
border-radius: 4px;
}
.result p {
margin: 5px 0;
font-size: 18px;
color: #2ecc71;
}
.preset-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.preset-table th {
background-color: #f2f2f2;
padding: 10px;
text-align: left;
position: relative;
}
.preset-table th button {
background: none;
border: none;
font-weight: bold;
font-size: inherit;
text-align: left;
padding: 0;
margin: 0;
width: 100%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
}
.preset-table th button:hover,
.preset-table th button:focus {
background-color: #e0e0e0;
outline: 2px solid #0066cc;
}
.sort-icon {
margin-left: 5px;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.preset-table td {
padding: 8px 10px;
border-bottom: 1px solid #ddd;
}
.preset-table tr:hover {
background-color: #f5f5f5;
}
.model-name-btn {
background-color: #e1f0ff;
border: 1px solid #d0e5fa;
border-radius: 4px;
font-size: 14px;
color: #333;
cursor: pointer;
text-align: left;
padding: 5px 8px;
font-family: inherit;
width: 100%;
text-decoration: none;
transition: background-color 0.2s, border-color 0.2s;
}
.model-name-btn:hover,
.model-name-btn:focus {
background-color: #96bce0;
border-color: #6a9ccf;
outline: 2px solid #0066cc;
}
.disclaimer {
font-size: 14px;
color: #777;
text-align: center;
font-style: italic;
}
</style>
<script defer data-domain="llm-prices.com" src="https://plausible.io/js/script.js"></script>
</head>
<body>
<div class="container">
<div class="flex-container">
<div class="calculator">
<h1>LLM pricing calculator</h1>
<div class="input-group">
<label for="inputTokens">Number of input tokens (aka prompt tokens):</label>
<input type="number" id="inputTokens" min="0">
</div>
<div class="input-group">
<label for="outputTokens">Number of output tokens (aka completion tokens):</label>
<input type="number" id="outputTokens" min="0">
</div>
<div class="input-group">
<label for="inputCost">Cost per million input tokens ($):</label>
<input type="number" id="inputCost" min="0" step="0.000001">
</div>
<div class="input-group">
<label for="outputCost">Cost per million output tokens ($):</label>
<input type="number" id="outputCost" min="0" step="0.000001">
</div>
<div class="result">
<p>Total cost: $<span id="costDollars">0.000000</span></p>
<p>Total cost: <span id="costCents">0</span> cents</p>
</div>
</div>
<div class="presets">
<h2>Model prices (per million tokens)</h2>
<div id="presetTable">
<table class="preset-table">
<thead>
<tr>
<th aria-sort="none">
<button type="button" id="sortByName">
Model
<span class="sort-icon" aria-hidden="true"></span>
</button>
</th>
<th aria-sort="ascending"> <!-- Default sort visually, JS will confirm -->
<button type="button" id="sortByInput">
Input cost
<span class="sort-icon" aria-hidden="true">↑</span>
</button>
</th>
<th aria-sort="none">
<button type="button" id="sortByOutput">
Output cost
<span class="sort-icon" aria-hidden="true"></span>
</button>
</th>
</tr>
</thead>
<tbody id="presetTableBody">
<!-- Table rows will be populated by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
<div class="disclaimer">Prices may have changed since this tool was last updated.</div>
</div>
<script>
// Prices are $ per 1M tokens
const presets = {
'gemini-2.5-pro-preview-03-25': { name: 'Gemini 2.5 Pro Preview ≤200k', input: 1.25, output: 10.00 },
'gemini-2.5-pro-preview-03-25-200k': { name: 'Gemini 2.5 Pro Preview >200k', input: 2.50, output: 15.00 },
'gemini-2.0-flash-lite': { name: 'Gemini 2.0 Flash Lite', input: 0.075, output: 0.30 },
'gemini-2.0-flash': { name: 'Gemini 2.0 Flash', input: 0.10, output: 0.40 },
'gemini-1.5-flash': { name: 'Gemini 1.5 Flash ≤128k', input: 0.075, output: 0.30 },
'gemini-1.5-flash-128k': { name: 'Gemini 1.5 Flash >128k', input: 0.15, output: 0.60 },
'gemini-1.5-flash-8b': { name: 'Gemini 1.5 Flash-8B ≤128k', input: 0.0375, output: 0.15 },
'gemini-1.5-flash-8b-128k': { name: 'Gemini 1.5 Flash-8B >128k', input: 0.075, output: 0.30 },
'gemini-1.5-pro': { name: 'Gemini 1.5 Pro ≤128k', input: 1.25, output: 5.00 },
'gemini-1.5-pro-128k': { name: 'Gemini 1.5 Pro >128k', input: 2.50, output: 10.00 },
'claude-3.7-sonnet': { name: 'Claude 3.7 Sonnet', input: 3.00, output: 15.00 },
'claude-3.5-sonnet': { name: 'Claude 3.5 Sonnet', input: 3.00, output: 15.00 },
'claude-3-opus': { name: 'Claude 3 Opus', input: 15.00, output: 75.00 },
'claude-3-haiku': { name: 'Claude 3 Haiku', input: 0.25, output: 1.25 },
'claude-3.5-haiku': { name: 'Claude 3.5 Haiku', input: 0.80, output: 4.00 },
'gpt-4.5': { name: 'GPT-4.5', input: 75.00, output: 150.00 },
'gpt-4o': { name: 'GPT-4o', input: 2.50, output: 10.00 },
'gpt-4o-mini': { name: 'GPT-4o Mini', input: 0.150, output: 0.600 },
'chatgpt-4o-latest': { name: 'ChatGPT 4o Latest', input: 5.00, output: 15.00 },
'o1-preview': { name: 'o1 and o1-preview', input: 15.00, output: 60.00 },
'o1-pro': { name: 'o1 Pro', input: 150.00, output: 600.00 },
'o1-mini': { name: 'o1-mini', input: 1.10, output: 4.40 },
'o3-mini': { name: 'o3-mini', input: 1.10, output: 4.40 },
'amazon-nova-micro': { name: 'Amazon Nova Micro', input: 0.035, output: 0.14 },
'amazon-nova-lite': { name: 'Amazon Nova Lite', input: 0.06, output: 0.24 },
'amazon-nova-pro': { name: 'Amazon Nova Pro', input: 0.8, output: 3.2 },
'deepseek-chat': {
name: 'DeepSeek Chat',
getPrice: () => {
const cutoffDate = new Date('2025-02-08T16:00:00Z');
const currentDate = new Date();
if (currentDate < cutoffDate) {
return { input: 0.14, output: 0.28 };
} else {
return { input: 0.27, output: 1.10 };
}
}
},
'deepseek-reasoner': { name: 'DeepSeek Reasoner', input: 0.55, output: 2.19 },
'pixtral-12b': { name: 'Pixtral 12B', input: 0.15, output: 0.15 },
'mistral-small-latest': { name: 'Mistral Small 3.1', input: 0.1, output: 0.3 },
'mistral-medium-2505': { name: 'Mistral Medium 3', input: 0.4, output: 2 },
'mistral-nemo': { name: 'Mistral NeMo', input: 0.15, output: 0.15 },
'open-mistral-7b': { name: 'Mistral 7B', input: 0.25, output: 0.25 },
'open-mixtral-8x7b': { name: 'Mixtral 8x7B', input: 0.7, output: 0.7 },
'open-mixtral-8x22b': { name: 'Mixtral 8x22B', input: 2, output: 6 },
'mistral-large-latest': { name: 'Mistral Large 24.11', input: 2, output: 6 },
'pixtral-large-latest': { name: 'Pixtral Large', input: 2, output: 6 },
'mistral-saba-latest': { name: 'Mistral Saba', input: 0.2, output: 0.6 },
'codestral-latest': { name: 'Codestral', input: 0.3, output: 0.9 },
'ministral-8b-latest': { name: 'Ministral 8B 24.10', input: 0.1, output: 0.1 },
'ministral-3b-latest': { name: 'Ministral 3B 24.10', input: 0.04, output: 0.04 },
'grok-3-beta': { name: 'Grok 3 Beta', input: 3.00, output: 15.00 },
'grok-3-mini-beta': { name: 'Grok 3 Mini Beta', input: 0.30, output: 0.50 },
'gpt-4.1': { name: 'GPT 4.1', input: 2.00, output: 8.00 },
'gpt-4.1-mini': { name: 'GPT 4.1 Mini', input: 0.40, output: 1.60 },
'gpt-4.1-nano': { name: 'GPT 4.1 Nano', input: 0.10, output: 0.40 },
'o3': { name: 'o3', input: 10, output: 40 },
'o4-mini': { name: 'o4-mini', input: 1.1, output: 4.4 },
'gemini-2.5-flash': { name: 'Gemini 2.5 Flash', input: 0.15, output: 0.60 },
'gemini-2.5-flash-thinking': { name: 'Gemini 2.5 Flash Thinking', input: 0.15, output: 3.50 },
};
// Global state variables for sorting
let currentSortColumn = 'input'; // Default sort column: 'name', 'input', or 'output'
let currentSortDirection = 'ascending'; // 'ascending' or 'descending'
// Form field IDs
const elIds = {
inputTokens: 'inputTokens',
outputTokens: 'outputTokens',
inputCost: 'inputCost',
outputCost: 'outputCost',
costDollars: 'costDollars',
costCents: 'costCents',
presetTableBody: 'presetTableBody',
sortByName: 'sortByName',
sortByInput: 'sortByInput',
sortByOutput: 'sortByOutput'
};
// Hash parameter keys (short for brevity in URL)
const HASH_KEYS = {
inputTokens: 'it',
outputTokens: 'ot',
inputCost: 'ic',
outputCost: 'oc',
sortBy: 'sb',
sortDir: 'sd'
};
function updateUrlHash() {
const params = new URLSearchParams();
const inputTokensVal = document.getElementById(elIds.inputTokens).value;
const outputTokensVal = document.getElementById(elIds.outputTokens).value;
const inputCostVal = document.getElementById(elIds.inputCost).value;
const outputCostVal = document.getElementById(elIds.outputCost).value;
if (inputTokensVal && parseFloat(inputTokensVal) !== 0) params.set(HASH_KEYS.inputTokens, inputTokensVal);
if (outputTokensVal && parseFloat(outputTokensVal) !== 0) params.set(HASH_KEYS.outputTokens, outputTokensVal);
if (inputCostVal && parseFloat(inputCostVal) !== 0) params.set(HASH_KEYS.inputCost, inputCostVal);
if (outputCostVal && parseFloat(outputCostVal) !== 0) params.set(HASH_KEYS.outputCost, outputCostVal);
// Only add sort params if not the default sort to keep URL cleaner for default state
if (!(currentSortColumn === 'input' && currentSortDirection === 'ascending')) {
params.set(HASH_KEYS.sortBy, currentSortColumn);
params.set(HASH_KEYS.sortDir, currentSortDirection);
}
const hashString = params.toString();
if (hashString) {
history.replaceState(null, '', '#' + hashString);
} else {
history.replaceState(null, '', window.location.pathname + window.location.search);
}
}
function calculateCost() {
const inputTokens = parseFloat(document.getElementById(elIds.inputTokens).value) || 0;
const outputTokens = parseFloat(document.getElementById(elIds.outputTokens).value) || 0;
const inputCost = parseFloat(document.getElementById(elIds.inputCost).value) || 0;
const outputCost = parseFloat(document.getElementById(elIds.outputCost).value) || 0;
const totalCost = (inputTokens * inputCost / 1000000) + (outputTokens * outputCost / 1000000);
document.getElementById(elIds.costDollars).textContent = totalCost.toFixed(6);
document.getElementById(elIds.costCents).textContent = trimZeros((totalCost * 100).toFixed(4));
updateUrlHash();
}
function trimZeros(numStr) {
return numStr.replace(/\.?0+$/, '');
}
function setPreset(modelKey) {
const preset = presets[modelKey];
if (!preset) return;
let prices;
if (preset.getPrice) {
prices = preset.getPrice();
} else {
prices = { input: preset.input, output: preset.output };
}
document.getElementById(elIds.inputCost).value = prices.input;
document.getElementById(elIds.outputCost).value = prices.output;
calculateCost(); // This will also call updateUrlHash
}
function formatPrice(price) {
if (Number.isInteger(price)) return `$${price}`;
const priceStr = price.toString();
if (priceStr.split('.')[1]?.length > 2 && !priceStr.endsWith('0')) return `$${priceStr}`;
return `$${price.toFixed(2)}`;
}
function getSortedPresetList(sortCol, sortDir) {
const presetList = [];
for (const [key, preset] of Object.entries(presets)) {
let prices;
if (preset.getPrice) {
prices = preset.getPrice();
} else {
prices = { input: preset.input, output: preset.output };
}
presetList.push({
key: key,
name: preset.name,
input: prices.input,
output: prices.output
});
}
presetList.sort((a, b) => {
let valA, valB;
if (sortCol === 'name') {
valA = a.name.toLowerCase(); // Case-insensitive sort for names
valB = b.name.toLowerCase();
if (valA < valB) return sortDir === 'ascending' ? -1 : 1;
if (valA > valB) return sortDir === 'ascending' ? 1 : -1;
return 0;
} else if (sortCol === 'input') {
valA = a.input;
valB = b.input;
} else if (sortCol === 'output') {
valA = a.output;
valB = b.output;
} else {
return 0; // Should not happen with valid sortCol
}
return sortDir === 'ascending' ? valA - valB : valB - valA;
});
return presetList;
}
function renderTable(data) {
const presetTableBody = document.getElementById(elIds.presetTableBody);
presetTableBody.innerHTML = ''; // Clear existing rows
data.forEach(item => {
const row = document.createElement('tr');
// Note: onclick now calls window.setPreset because these are dynamically added
row.innerHTML = `
<td><button class="model-name-btn" onclick="window.setPreset('${item.key}')">${item.name}</button></td>
<td>${formatPrice(item.input)}</td>
<td>${formatPrice(item.output)}</td>
`;
presetTableBody.appendChild(row);
});
}
function updateTableSortVisualsAndData() {
const thElements = document.querySelectorAll('.preset-table th');
thElements.forEach(th => {
th.setAttribute('aria-sort', 'none');
th.querySelector('.sort-icon').textContent = '';
});
let activeThButtonId;
if (currentSortColumn === 'name') activeThButtonId = elIds.sortByName;
else if (currentSortColumn === 'input') activeThButtonId = elIds.sortByInput;
else if (currentSortColumn === 'output') activeThButtonId = elIds.sortByOutput;
if (activeThButtonId) {
const activeTh = document.getElementById(activeThButtonId)?.closest('th');
if (activeTh) {
activeTh.setAttribute('aria-sort', currentSortDirection);
activeTh.querySelector('.sort-icon').textContent = (currentSortDirection === 'ascending') ? '↑' : '↓';
}
}
const sortedList = getSortedPresetList(currentSortColumn, currentSortDirection);
renderTable(sortedList);
}
function sortTable(column) {
if (currentSortColumn === column) {
currentSortDirection = (currentSortDirection === 'ascending') ? 'descending' : 'ascending';
} else {
currentSortColumn = column;
// Default direction for a new column (name: asc, numeric: asc)
currentSortDirection = 'ascending';
}
updateTableSortVisualsAndData();
updateUrlHash();
}
function applyStateFromHash() {
if (!window.location.hash || window.location.hash === '#') {
return; // No hash, use defaults
}
const params = new URLSearchParams(window.location.hash.substring(1));
const inputTokensEl = document.getElementById(elIds.inputTokens);
const outputTokensEl = document.getElementById(elIds.outputTokens);
const inputCostEl = document.getElementById(elIds.inputCost);
const outputCostEl = document.getElementById(elIds.outputCost);
if (params.has(HASH_KEYS.inputTokens)) inputTokensEl.value = params.get(HASH_KEYS.inputTokens);
if (params.has(HASH_KEYS.outputTokens)) outputTokensEl.value = params.get(HASH_KEYS.outputTokens);
if (params.has(HASH_KEYS.inputCost)) inputCostEl.value = params.get(HASH_KEYS.inputCost);
if (params.has(HASH_KEYS.outputCost)) outputCostEl.value = params.get(HASH_KEYS.outputCost);
const sortByFromHash = params.get(HASH_KEYS.sortBy);
const sortDirFromHash = params.get(HASH_KEYS.sortDir);
// Valid sort columns and directions
const validSortCols = ['name', 'input', 'output'];
const validSortDirs = ['ascending', 'descending'];
if (sortByFromHash && validSortCols.includes(sortByFromHash) &&
sortDirFromHash && validSortDirs.includes(sortDirFromHash)) {
currentSortColumn = sortByFromHash;
currentSortDirection = sortDirFromHash;
} else if (sortByFromHash || sortDirFromHash) {
// If partial or invalid sort params, revert to default to avoid broken state
currentSortColumn = 'input';
currentSortDirection = 'ascending';
}
// If no sort params in hash, defaults currentSortColumn/Direction are used
}
function createPresetsTableStructure() {
// Event listeners for sort buttons
document.getElementById(elIds.sortByName).addEventListener('click', () => sortTable('name'));
document.getElementById(elIds.sortByInput).addEventListener('click', () => sortTable('input'));
document.getElementById(elIds.sortByOutput).addEventListener('click', () => sortTable('output'));
// Initial render based on currentSortColumn and currentSortDirection
// (which might have been updated by applyStateFromHash)
updateTableSortVisualsAndData();
}
function handlePaste(event) {
const text = (event.clipboardData || window.clipboardData).getData('text');
const comboMatch = text.match(/([\d,]+)\s*input[, ]+([\d,]+)\s*output/i);
if (comboMatch) {
event.preventDefault();
document.getElementById(elIds.inputTokens).value = comboMatch[1].replace(/,/g, '');
document.getElementById(elIds.outputTokens).value = comboMatch[2].replace(/,/g, '');
calculateCost();
return;
}
const numMatch = text.match(/^\s*([\d,]+)\s*$/);
if (numMatch && event.target.type === 'number') { // Ensure it's a number input
event.preventDefault();
event.target.value = numMatch[1].replace(/,/g, '');
// Manually trigger 'input' event if needed, or just calculate
calculateCost();
}
}
// Make setPreset globally accessible for dynamically created buttons
window.setPreset = setPreset;
// Initialize on DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
applyStateFromHash(); // 1. Apply state from URL hash first (updates globals)
createPresetsTableStructure(); // 2. Setup table structure & initial render using (potentially updated) globals
calculateCost(); // 3. Initial calculation and URL hash update based on current state
// Add event listeners to form inputs
const formInputs = [elIds.inputTokens, elIds.outputTokens, elIds.inputCost, elIds.outputCost];
formInputs.forEach(id => {
document.getElementById(id).addEventListener('input', calculateCost);
// Add paste listener only to token inputs as per original logic
if (id === elIds.inputTokens || id === elIds.outputTokens) {
document.getElementById(id).addEventListener('paste', handlePaste);
} else {
document.getElementById(id).addEventListener('paste', (e) => {
// Basic paste handling for cost inputs if desired, or just let browser handle
setTimeout(calculateCost, 0); // Recalculate after paste
});
}
});
// Specific paste handler for inputTokens - remove from generic loop if it's only for one.
// Original code had it only for inputTokens. Let's keep it for inputTokens only for now.
document.getElementById(elIds.inputTokens).removeEventListener('paste', handlePaste); // remove generic one
document.getElementById(elIds.inputTokens).addEventListener('paste', handlePaste); // add specific one
});
</script>
</body>
</html>
Key changes made:
- Global Sort State:
currentSortColumn
andcurrentSortDirection
variables store the table's sort state. They default toinput
andascending
. - URL Hash Management:
HASH_KEYS
: Defines short keys for URL parameters (e.g.,it
forinputTokens
).updateUrlHash()
: Serializes form values and current sort state into the URL hash usinghistory.replaceState
. It tries to keep the URL clean by omitting default sort parameters.applyStateFromHash()
: Parses the URL hash on page load. If parameters are found, it populates form fields and updates the global sort state variables. It includes basic validation for sort parameters.
- Modified Functions:
calculateCost()
: Now callsupdateUrlHash()
after performing calculations.setPreset()
: CallscalculateCost()
, which in turn updates the hash. (Made globally accessible viawindow.setPreset
for buttons added viainnerHTML
).sortTable(column)
: UpdatescurrentSortColumn
andcurrentSortDirection
, then callsupdateTableSortVisualsAndData()
to re-render the table, and finallyupdateUrlHash()
.getSortedPresetList(sortCol, sortDir)
: New helper function to get the sorted list of presets based on current sort criteria.renderTable(data)
: Renders the table rows.onclick
for model buttons now callswindow.setPreset
.updateTableSortVisualsAndData()
: New helper to updatearia-sort
attributes on table headers, set sort icons, and re-render the table usinggetSortedPresetList
andrenderTable
.createPresetsTableStructure()
: (Renamed fromcreatePresetsTable
in spirit, as originalcreatePresetsTable
did more). This function now primarily sets up sort button event listeners and callsupdateTableSortVisualsAndData()
for the initial table render.
- Event Listeners:
- Inline
onkeyup
/onchange
handlers removed from HTML input fields. addEventListener('input', calculateCost)
is now used for the four numeric input fields, triggeringcalculateCost
(and thusupdateUrlHash
) on any input.- Paste handlers are set up. The specific one for
inputTokens
is retained.
- Inline
- Initialization (
DOMContentLoaded
):applyStateFromHash()
is called first to read any existing state from the URL.createPresetsTableStructure()
is called to set up the table, which will render based on the (potentially hash-modified) global sort state.calculateCost()
is called to perform an initial calculation (using potentially hash-modified form values) and update the URL hash to reflect the fully resolved initial state.
- Constants:
elIds
for element IDs andHASH_KEYS
for URL parameter names are used for better maintainability.
This setup ensures that the form state and table sort order are reflected in the URL, applied on load, and updated dynamically without full page reloads.
5,883 input, 7,758 output, {"promptTokensDetails": [{"modality": "TEXT", "tokenCount": 5883}], "thoughtsTokenCount": 8329}