| |
| |
| |
| |
| |
| |
|
|
| (function () { |
| 'use strict'; |
|
|
| var API = '/api/pricing'; |
| var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; |
|
|
| function esc(s) { |
| return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { |
| return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; |
| }); |
| } |
|
|
| function fmtPrice(n) { |
| if (n == null) return '—'; |
| if (n < 0.01) return '$' + n.toFixed(4); |
| if (n < 1) return '$' + n.toFixed(3); |
| return '$' + n.toFixed(2); |
| } |
|
|
| function fmtCredits(n) { |
| return Number(n || 0).toLocaleString(); |
| } |
|
|
| |
| |
| function shortName(full) { |
| var n = full.replace(/^free:/, ''); |
| var slash = n.indexOf('/'); |
| return slash >= 0 ? n.slice(slash + 1) : n; |
| } |
|
|
| function providerOf(full) { |
| var n = full.replace(/^free:/, ''); |
| var slash = n.indexOf('/'); |
| return slash >= 0 ? n.slice(0, slash) : ''; |
| } |
|
|
| var STATE = { models: [], rate: null }; |
|
|
| async function load() { |
| var body = document.getElementById('rates-body'); |
| try { |
| var res = await fetch(API); |
| if (!res.ok) throw new Error('status ' + res.status); |
| var data = await res.json(); |
| STATE.models = data.models || []; |
| STATE.rate = data.rate || { credits_per_usd: 10000, input_ratio: 1 / 3 }; |
| renderTable(STATE.models); |
| populateCalculator(STATE.models); |
| } catch (e) { |
| if (body) body.innerHTML = '<tr><td colspan="5" class="rates-empty">Couldn\u2019t load rates. Please try again.</td></tr>'; |
| } |
| } |
|
|
| function renderTable(models) { |
| var body = document.getElementById('rates-body'); |
| if (!body) return; |
| if (!models.length) { |
| body.innerHTML = '<tr><td colspan="5" class="rates-empty">No models match your filter.</td></tr>'; |
| return; |
| } |
| var html = ''; |
| for (var i = 0; i < models.length; i++) { |
| var m = models[i]; |
| var prov = providerOf(m.model); |
| var badge = m.free ? '<span class="free-badge">free</span>' : ''; |
| html += |
| '<tr>' + |
| '<td><span class="model-name" title="' + esc(m.model) + '">' + esc(shortName(m.model)) + '</span>' + badge + |
| (prov ? '<div style="font-size:.7rem;color:var(--ink-soft,#6b5b7a);">' + esc(prov) + '</div>' : '') + '</td>' + |
| '<td class="num">' + fmtPrice(m.usd_per_1m_input) + '</td>' + |
| '<td class="num">' + fmtPrice(m.usd_per_1m_output) + '</td>' + |
| '<td class="num">' + fmtCredits(m.credits_per_1k_output) + '</td>' + |
| '<td class="num">' + fmtCredits(m.targets) + '</td>' + |
| '</tr>'; |
| } |
| body.innerHTML = html; |
| } |
|
|
| function populateCalculator(models) { |
| var sel = document.getElementById('calc-model'); |
| if (!sel) return; |
| |
| var sorted = models.slice().sort(function (a, b) { |
| if (a.free !== b.free) return a.free ? 1 : -1; |
| return b.usd_per_1m_output - a.usd_per_1m_output; |
| }); |
| var opts = ''; |
| for (var i = 0; i < sorted.length; i++) { |
| opts += '<option value="' + esc(sorted[i].model) + '">' + esc(sorted[i].model) + '</option>'; |
| } |
| sel.innerHTML = opts; |
| recalc(); |
| } |
|
|
| function lookup(name) { |
| for (var i = 0; i < STATE.models.length; i++) { |
| if (STATE.models[i].model === name) return STATE.models[i]; |
| } |
| return null; |
| } |
|
|
| function recalc() { |
| var sel = document.getElementById('calc-model'); |
| var inEl = document.getElementById('calc-in'); |
| var outEl = document.getElementById('calc-out'); |
| var credEl = document.getElementById('calc-result').querySelector('.cr-credits'); |
| var usdEl = document.getElementById('calc-usd'); |
| if (!sel || !inEl || !outEl || !credEl) return; |
|
|
| var m = lookup(sel.value); |
| var tokIn = Math.max(0, parseInt(inEl.value, 10) || 0); |
| var tokOut = Math.max(0, parseInt(outEl.value, 10) || 0); |
| if (!m) { credEl.textContent = '—'; usdEl.textContent = ''; return; } |
|
|
| |
| var inPrice = m.usd_per_1m_output * STATE.rate.input_ratio; |
| var usd = (tokIn * inPrice + tokOut * m.usd_per_1m_output) / 1e6; |
| var credits = Math.round(usd * (STATE.rate.credits_per_usd || 10000)); |
| if ((tokIn || tokOut) && credits < 1) credits = 1; |
| if (!tokIn && !tokOut) credits = 0; |
|
|
| credEl.textContent = fmtCredits(credits); |
| usdEl.textContent = credits > 0 ? ('\u2248 $' + (credits / (STATE.rate.credits_per_usd || 10000)).toFixed(4) + ' USD') : ''; |
| } |
|
|
| |
| function init() { |
| load(); |
| var sel = document.getElementById('calc-model'); |
| var inEl = document.getElementById('calc-in'); |
| var outEl = document.getElementById('calc-out'); |
| var search = document.getElementById('rate-search'); |
| if (sel) sel.addEventListener('change', recalc); |
| if (inEl) inEl.addEventListener('input', recalc); |
| if (outEl) outEl.addEventListener('input', recalc); |
| if (search) search.addEventListener('input', function () { |
| var q = search.value.trim().toLowerCase(); |
| if (!q) { renderTable(STATE.models); return; } |
| renderTable(STATE.models.filter(function (m) { |
| return m.model.toLowerCase().indexOf(q) >= 0; |
| })); |
| }); |
| } |
|
|
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', init); |
| } else { |
| init(); |
| } |
| })(); |
|
|