Simple token tracker for vultr Serverless Inference Track your token use and current session.
Preview
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Vultr Inference Usage Monitor</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| background: white; | |
| border-radius: 15px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| padding: 40px; | |
| max-width: 800px; | |
| width: 100%; | |
| } | |
| .test-mode { | |
| background: #fffbe6; | |
| border: 2px solid #ffa500; | |
| color: #ff8c00; | |
| padding: 12px; | |
| border-radius: 8px; | |
| text-align: center; | |
| font-weight: bold; | |
| margin-bottom: 20px; | |
| display: none; | |
| } | |
| h1 { | |
| text-align: center; | |
| color: #333; | |
| font-size: 32px; | |
| margin-bottom: 10px; | |
| } | |
| .timestamp { | |
| text-align: center; | |
| color: #666; | |
| font-size: 14px; | |
| margin-bottom: 30px; | |
| } | |
| .info-section { | |
| margin-bottom: 25px; | |
| } | |
| .label { | |
| color: #555; | |
| font-size: 16px; | |
| margin-bottom: 8px; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 35px; | |
| background: #e0e0e0; | |
| border-radius: 17px; | |
| overflow: hidden; | |
| position: relative; | |
| margin-bottom: 15px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #4caf50 0%, #45a049 100%); | |
| transition: width 0.5s ease, background 0.3s ease; | |
| border-radius: 17px; | |
| } | |
| .progress-fill.overage { | |
| background: linear-gradient(90deg, #f44336 0%, #d32f2f 100%); | |
| } | |
| .separator { | |
| height: 2px; | |
| background: #ddd; | |
| margin: 30px 0; | |
| } | |
| .total-cost { | |
| text-align: center; | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: #9c27b0; | |
| padding: 20px; | |
| background: #f5f5f5; | |
| border-radius: 10px; | |
| margin-top: 20px; | |
| } | |
| .error { | |
| color: #f44336; | |
| text-align: center; | |
| padding: 15px; | |
| background: #ffebee; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| display: none; | |
| } | |
| .config-section { | |
| background: #f9f9f9; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-bottom: 30px; | |
| } | |
| .config-row { | |
| display: flex; | |
| gap: 20px; | |
| margin-bottom: 15px; | |
| align-items: center; | |
| } | |
| .config-row label { | |
| flex: 0 0 150px; | |
| font-weight: 500; | |
| } | |
| .config-row input { | |
| flex: 1; | |
| padding: 8px 12px; | |
| border: 1px solid #ddd; | |
| border-radius: 5px; | |
| font-size: 14px; | |
| } | |
| .config-row button { | |
| padding: 8px 20px; | |
| background: #667eea; | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: background 0.3s; | |
| } | |
| .config-row button:hover { | |
| background: #5568d3; | |
| } | |
| .overage-text { | |
| color: #f44336; | |
| font-weight: bold; | |
| } | |
| .rate-info { | |
| background: #e8f4f8; | |
| padding: 15px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| border-left: 4px solid #2196F3; | |
| } | |
| .rate-info .label { | |
| color: #1976D2; | |
| font-weight: 500; | |
| } | |
| .session-section { | |
| background: #f3e5f5; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-top: 30px; | |
| border-left: 4px solid #9c27b0; | |
| } | |
| .session-section h3 { | |
| color: #7b1fa2; | |
| margin-bottom: 15px; | |
| } | |
| .session-button { | |
| background: #9c27b0; | |
| color: white; | |
| padding: 10px 25px; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| font-weight: 500; | |
| transition: background 0.3s; | |
| margin-bottom: 15px; | |
| } | |
| .session-button:hover { | |
| background: #7b1fa2; | |
| } | |
| .session-button.stop { | |
| background: #f44336; | |
| } | |
| .session-button.stop:hover { | |
| background: #d32f2f; | |
| } | |
| .session-stats { | |
| display: none; | |
| margin-top: 15px; | |
| } | |
| .session-stats.active { | |
| display: block; | |
| } | |
| .stat-row { | |
| padding: 8px 0; | |
| border-bottom: 1px solid #e1bee7; | |
| } | |
| .stat-row:last-child { | |
| border-bottom: none; | |
| } | |
| .stat-label { | |
| font-weight: 500; | |
| color: #7b1fa2; | |
| } | |
| .stat-value { | |
| color: #555; | |
| margin-left: 10px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="test-mode" id="testModeAlert"></div> | |
| <div class="error" id="errorMessage"></div> | |
| <!-- Full JSON Data Details --> | |
| <details id="jsonDetails" style="margin-top:20px;"> | |
| <summary style="cursor:pointer;padding:8px 16px;background:#667eea;color:#fff;border:none;border-radius:5px;">Show Full JSON Data</summary> | |
| <div id="jsonContainer" style="margin-top:15px;padding:10px;background:#f5f5f5;border-radius:5px;overflow:auto;max-height:300px;"></div> | |
| </details> | |
| <div class="config-section"> | |
| <h3 style="margin-bottom: 15px; color: #333;">Configuration</h3> | |
| <div class="config-row"> | |
| <label>API Key:</label> | |
| <input type="password" id="apiKey" placeholder="Enter your Vultr API key"> | |
| </div> | |
| <div class="config-row"> | |
| <label>Refresh (seconds):</label> | |
| <input type="number" id="refreshInterval" value="5" min="1"> | |
| </div> | |
| <div class="config-row"> | |
| <label>Test Mode Tokens:</label> | |
| <input type="number" id="testTokens" value="0" min="0" placeholder="0 = use real API"> | |
| <button id="startBtn" onclick="startMonitoring()">Start Monitoring</button> | |
| <button id="stopBtn" onclick="stopMonitoring()" style="background: #f44336; display: none;">Stop Monitoring</button> | |
| </div> | |
| </div> | |
| <h1>Vultr Inference Usage Monitor</h1> | |
| <div class="timestamp" id="timestamp">Last updated: Never</div> | |
| <div class="info-section"> | |
| <div class="label" id="tokensLabel">Chat tokens used: 0 / 50,000,000</div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="tokenProgress"></div> | |
| </div> | |
| </div> | |
| <div class="info-section"> | |
| <div class="label" id="percentLabel">Percentage used: 0.00%</div> | |
| <div class="label" id="remainingLabel">Tokens remaining: 50,000,000</div> | |
| </div> | |
| <div class="separator"></div> | |
| <div class="info-section"> | |
| <div class="label" id="proportionalLabel">Proportional value used: $0.0000 of $10.00</div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="costProgress"></div> | |
| </div> | |
| </div> | |
| <div class="rate-info"> | |
| <div class="label">Base monthly cost: $10.00 (includes 50M tokens)</div> | |
| <div class="label">Cost per 1K tokens over limit: $0.0002</div> | |
| <div class="label">Cost per 1M tokens over limit: $0.20</div> | |
| </div> | |
| <div class="info-section"> | |
| <div class="label" id="overageLabel">Total overage cost: $0.0000</div> | |
| </div> | |
| <div class="total-cost" id="totalCost">Total cost this month: $10.00</div> | |
| <div class="session-section"> | |
| <h3>Session Tracking</h3> | |
| <button class="session-button" id="sessionBtn" onclick="toggleSessionTracking()">Start Tracking Session</button> | |
| <div class="session-stats" id="sessionStats"> | |
| <div class="stat-row"> | |
| <span class="stat-label">Session Duration:</span> | |
| <span class="stat-value" id="sessionDuration">0h 0m 0s</span> | |
| </div> | |
| <div class="stat-row"> | |
| <span class="stat-label">Tokens Used in Session:</span> | |
| <span class="stat-value" id="sessionTokens">0</span> | |
| </div> | |
| <div class="stat-row"> | |
| <span class="stat-label">Session Cost:</span> | |
| <span class="stat-value" id="sessionCost">$0.0000</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const MONTHLY_LIMIT = 50000000; | |
| const BASE_COST = 10.00; | |
| const OVERAGE_RATE = 0.0002 / 1000; | |
| const API_URL = "https://api.vultrinference.com/v1/usage"; | |
| let intervalId = null; | |
| let sessionTracking = false; | |
| let sessionStartTokens = 0; | |
| let sessionStartTime = null; | |
| let sessionIntervalId = null; | |
| function formatNumber(num) { | |
| return new Intl.NumberFormat('en-US').format(num); | |
| } | |
| function showError(message) { | |
| const errorEl = document.getElementById('errorMessage'); | |
| errorEl.textContent = 'Error: ' + message; | |
| errorEl.style.display = 'block'; | |
| setTimeout(() => { | |
| errorEl.style.display = 'none'; | |
| }, 5000); | |
| } | |
| async function fetchUsageData() { | |
| const apiKey = document.getElementById('apiKey').value.trim(); | |
| const testTokens = parseInt(document.getElementById('testTokens').value) || 0; | |
| let chatUsed; | |
| if (testTokens > 0) { | |
| chatUsed = testTokens; | |
| const testAlert = document.getElementById('testModeAlert'); | |
| testAlert.textContent = `⚠️ TEST MODE ACTIVE - Using simulated token amount: ${formatNumber(testTokens)} ⚠️`; | |
| testAlert.style.display = 'block'; | |
| // Simulated full data for test mode | |
| var fullData = { | |
| usage: { | |
| current_month: { | |
| chat: testTokens, | |
| tts: 0, | |
| tts_sm: 0, | |
| image: 0, | |
| image_sm: 0 | |
| }, | |
| previous_month: { | |
| chat: 0, | |
| tts: 0, | |
| tts_sm: 0, | |
| image: 0, | |
| image_sm: 0 | |
| } | |
| } | |
| }; | |
| updateJSONTab(fullData); | |
| } else { | |
| if (!apiKey) { | |
| showError('Please enter your API key'); | |
| return; | |
| } | |
| document.getElementById('testModeAlert').style.display = 'none'; | |
| try { | |
| const response = await fetch(API_URL, { | |
| method: 'GET', | |
| headers: { | |
| 'Authorization': `Bearer ${apiKey}` | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`API request failed: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| chatUsed = parseFloat(data.usage.current_month.chat); | |
| // Store full JSON for secondary tab | |
| updateJSONTab(data); | |
| } catch (error) { | |
| showError(error.message); | |
| return; | |
| } | |
| } | |
| updateUI(chatUsed); | |
| } | |
| // Populate the secondary JSON tab with formatted HTML | |
| function updateJSONTab(data) { | |
| const container = document.getElementById('jsonContainer'); | |
| if (!container) return; | |
| // Build HTML representation | |
| const html = renderJSONToHTML(data); | |
| container.innerHTML = html; | |
| // Do NOT auto-open the details; user controls visibility | |
| } | |
| // Convert the JSON usage object into a nice HTML table | |
| function renderJSONToHTML(data) { | |
| if (!data || !data.usage) return '<p>No usage data available.</p>'; | |
| const { current_month, previous_month } = data.usage; | |
| // Helper to render a month table | |
| const renderMonth = (monthData, title) => { | |
| let rows = ''; | |
| for (const [key, value] of Object.entries(monthData)) { | |
| rows += ` | |
| <tr> | |
| <td style="padding:4px 8px;font-weight:500;">${key}</td> | |
| <td style="padding:4px 8px;text-align:right;">${value}</td> | |
| </tr>`; | |
| } | |
| return ` | |
| <h4 style="margin:8px 0 4px;color:#333;">${title}</h4> | |
| <table style="width:100%;border-collapse:collapse;margin-bottom:12px;"> | |
| <thead> | |
| <tr> | |
| <th style="text-align:left;padding:4px 8px;background:#e0e0e0;">Metric</th> | |
| <th style="text-align:right;padding:4px 8px;background:#e0e0e0;">Count</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${rows} | |
| </tbody> | |
| </table>`; | |
| }; | |
| return ` | |
| <div style="font-family:Arial,Helvetica,sans-serif;color:#222;"> | |
| ${renderMonth(current_month, 'Current Month')} | |
| ${renderMonth(previous_month, 'Previous Month')} | |
| </div>`; | |
| } | |
| function updateUI(chatUsed) { | |
| const percentUsed = (chatUsed / MONTHLY_LIMIT) * 100; | |
| const tokensRemaining = MONTHLY_LIMIT - chatUsed; | |
| const proportionalValue = (chatUsed / MONTHLY_LIMIT) * BASE_COST; | |
| let totalCost = BASE_COST; | |
| let overCost = 0; | |
| // Update timestamp | |
| const now = new Date(); | |
| document.getElementById('timestamp').textContent = | |
| `Last updated: ${now.toLocaleTimeString()}`; | |
| // Update token labels | |
| document.getElementById('tokensLabel').textContent = | |
| `Chat tokens used: ${formatNumber(chatUsed)} / ${formatNumber(MONTHLY_LIMIT)}`; | |
| document.getElementById('percentLabel').textContent = | |
| `Percentage used: ${percentUsed.toFixed(2)}%`; | |
| document.getElementById('remainingLabel').textContent = | |
| `Tokens remaining: ${formatNumber(tokensRemaining)}`; | |
| document.getElementById('proportionalLabel').textContent = | |
| `Proportional value used: $${proportionalValue.toFixed(4)} of $10.00`; | |
| // Update progress bars | |
| const tokenProgress = document.getElementById('tokenProgress'); | |
| const costProgress = document.getElementById('costProgress'); | |
| const progressPercent = Math.min(percentUsed, 100); | |
| tokenProgress.style.width = progressPercent + '%'; | |
| costProgress.style.width = progressPercent + '%'; | |
| // Set progress bar color based on usage percentage | |
| let barColor; | |
| if (percentUsed < 75) { | |
| barColor = '#4caf50'; // green | |
| } else if (percentUsed < 85) { | |
| barColor = '#ffeb3b'; // yellow | |
| } else if (percentUsed < 95) { | |
| barColor = '#ff9800'; // orange | |
| } else { | |
| barColor = '#f44336'; // red | |
| } | |
| tokenProgress.style.background = barColor; | |
| costProgress.style.background = barColor; | |
| // Handle overage | |
| const overageLabel = document.getElementById('overageLabel'); | |
| if (chatUsed > MONTHLY_LIMIT) { | |
| const overBy = chatUsed - MONTHLY_LIMIT; | |
| overCost = overBy * OVERAGE_RATE; | |
| totalCost += overCost; | |
| overageLabel.innerHTML = `<span class="overage-text">Total overage cost: $${overCost.toFixed(4)} (${formatNumber(overBy)} tokens over limit)</span>`; | |
| tokenProgress.classList.add('overage'); | |
| costProgress.classList.add('overage'); | |
| } else { | |
| overageLabel.innerHTML = 'Total overage cost: $0.0000'; | |
| tokenProgress.classList.remove('overage'); | |
| costProgress.classList.remove('overage'); | |
| } | |
| // Update total cost | |
| document.getElementById('totalCost').textContent = | |
| `Total cost this month: ${totalCost.toFixed(2)}`; | |
| // Update session tracking if active | |
| if (sessionTracking) { | |
| updateSessionStats(chatUsed); | |
| } | |
| } | |
| function startMonitoring() { | |
| // Clear existing interval | |
| if (intervalId) { | |
| clearInterval(intervalId); | |
| } | |
| // Fetch immediately | |
| fetchUsageData(); | |
| // Set up recurring fetch | |
| const refreshSeconds = parseInt(document.getElementById('refreshInterval').value) || 5; | |
| intervalId = setInterval(fetchUsageData, refreshSeconds * 1000); | |
| // Toggle buttons | |
| document.getElementById('startBtn').style.display = 'none'; | |
| document.getElementById('stopBtn').style.display = 'inline-block'; | |
| } | |
| function stopMonitoring() { | |
| // Clear interval | |
| if (intervalId) { | |
| clearInterval(intervalId); | |
| intervalId = null; | |
| } | |
| // Toggle buttons | |
| document.getElementById('startBtn').style.display = 'inline-block'; | |
| document.getElementById('stopBtn').style.display = 'none'; | |
| // Update timestamp | |
| document.getElementById('timestamp').textContent = 'Monitoring stopped'; | |
| } | |
| function toggleSessionTracking() { | |
| const btn = document.getElementById('sessionBtn'); | |
| const stats = document.getElementById('sessionStats'); | |
| if (!sessionTracking) { | |
| // Start tracking | |
| sessionTracking = true; | |
| sessionStartTokens = getCurrentTokenCount(); | |
| sessionStartTime = new Date(); | |
| btn.textContent = 'Stop Tracking Session'; | |
| btn.classList.add('stop'); | |
| stats.classList.add('active'); | |
| // Update session duration every second | |
| sessionIntervalId = setInterval(updateSessionDuration, 1000); | |
| } else { | |
| // Stop tracking | |
| sessionTracking = false; | |
| btn.textContent = 'Start Tracking Session'; | |
| btn.classList.remove('stop'); | |
| if (sessionIntervalId) { | |
| clearInterval(sessionIntervalId); | |
| sessionIntervalId = null; | |
| } | |
| } | |
| } | |
| function getCurrentTokenCount() { | |
| const text = document.getElementById('tokensLabel').textContent; | |
| const match = text.match(/Chat tokens used: ([\d,]+)/); | |
| if (match) { | |
| return parseInt(match[1].replace(/,/g, '')); | |
| } | |
| return 0; | |
| } | |
| function updateSessionDuration() { | |
| if (!sessionStartTime) return; | |
| const now = new Date(); | |
| const diff = Math.floor((now - sessionStartTime) / 1000); // seconds | |
| const hours = Math.floor(diff / 3600); | |
| const minutes = Math.floor((diff % 3600) / 60); | |
| const seconds = diff % 60; | |
| document.getElementById('sessionDuration').textContent = | |
| `${hours}h ${minutes}m ${seconds}s`; | |
| } | |
| function updateSessionStats(currentTokens) { | |
| const tokensUsed = currentTokens - sessionStartTokens; | |
| // Calculate cost | |
| let sessionCost = 0; | |
| let basePortionUsed = 0; | |
| if (tokensUsed <= 0) { | |
| // No tokens used yet or negative (shouldn't happen) | |
| sessionCost = 0; | |
| basePortionUsed = 0; | |
| } else if (sessionStartTokens >= MONTHLY_LIMIT) { | |
| // Started already over limit, all overage | |
| sessionCost = tokensUsed * OVERAGE_RATE; | |
| basePortionUsed = 0; | |
| } else if (currentTokens <= MONTHLY_LIMIT) { | |
| // All within base limit | |
| basePortionUsed = (tokensUsed / MONTHLY_LIMIT) * BASE_COST; | |
| sessionCost = basePortionUsed; | |
| } else { | |
| // Crossed the limit during session | |
| const tokensInBase = MONTHLY_LIMIT - sessionStartTokens; | |
| const tokensInOverage = tokensUsed - tokensInBase; | |
| basePortionUsed = (tokensInBase / MONTHLY_LIMIT) * BASE_COST; | |
| const overageCost = tokensInOverage * OVERAGE_RATE; | |
| sessionCost = basePortionUsed + overageCost; | |
| } | |
| const basePercent = (basePortionUsed / BASE_COST) * 100; | |
| // Update display | |
| document.getElementById('sessionTokens').textContent = formatNumber(tokensUsed); | |
| document.getElementById('sessionCost').textContent = `${sessionCost.toFixed(4)}`; | |
| } | |
| // Auto-start if API key is present (you can remove this if you prefer manual start) | |
| window.addEventListener('load', () => { | |
| const savedKey = ''; // Your key from script | |
| document.getElementById('apiKey').value = savedKey; | |
| }); | |
| </script> | |
| </body> | |
| </html> |