Skip to content

Instantly share code, notes, and snippets.

@simonw

simonw/state.md Secret

Created May 7, 2025 20:25
Show Gist options
  • Save simonw/9d4e15b58ccfaca9e08747225cb69fa2 to your computer and use it in GitHub Desktop.
Save simonw/9d4e15b58ccfaca9e08747225cb69fa2 to your computer and use it in GitHub Desktop.

2025-05-07T20:18:53 conversation: 01jtp6j1neawkv8sqe22yrj4g4 id: 01jtp6j1nk7m1cxyvhszm8d8yp

Model: gemini-2.5-pro-preview-05-06

Prompt

-- none --

Prompt fragments

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>

System

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.

Response

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:

  1. Global Sort State: currentSortColumn and currentSortDirection variables store the table's sort state. They default to input and ascending.
  2. URL Hash Management:
    • HASH_KEYS: Defines short keys for URL parameters (e.g., it for inputTokens).
    • updateUrlHash(): Serializes form values and current sort state into the URL hash using history.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.
  3. Modified Functions:
    • calculateCost(): Now calls updateUrlHash() after performing calculations.
    • setPreset(): Calls calculateCost(), which in turn updates the hash. (Made globally accessible via window.setPreset for buttons added via innerHTML).
    • sortTable(column): Updates currentSortColumn and currentSortDirection, then calls updateTableSortVisualsAndData() to re-render the table, and finally updateUrlHash().
    • 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 calls window.setPreset.
    • updateTableSortVisualsAndData(): New helper to update aria-sort attributes on table headers, set sort icons, and re-render the table using getSortedPresetList and renderTable.
    • createPresetsTableStructure(): (Renamed from createPresetsTable in spirit, as original createPresetsTable did more). This function now primarily sets up sort button event listeners and calls updateTableSortVisualsAndData() for the initial table render.
  4. Event Listeners:
    • Inline onkeyup/onchange handlers removed from HTML input fields.
    • addEventListener('input', calculateCost) is now used for the four numeric input fields, triggering calculateCost (and thus updateUrlHash) on any input.
    • Paste handlers are set up. The specific one for inputTokens is retained.
  5. Initialization (DOMContentLoaded):
    1. applyStateFromHash() is called first to read any existing state from the URL.
    2. createPresetsTableStructure() is called to set up the table, which will render based on the (potentially hash-modified) global sort state.
    3. 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.
  6. Constants: elIds for element IDs and HASH_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.

Token usage:

5,883 input, 7,758 output, {"promptTokensDetails": [{"modality": "TEXT", "tokenCount": 5883}], "thoughtsTokenCount": 8329}

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