diff --git a/router.py b/router.py index 9a1f0c8..8c3be12 100644 --- a/router.py +++ b/router.py @@ -1196,21 +1196,27 @@ async def stats_proxy(request: Request, model: Optional[str] = None): # Get time series data for the last 30 days (43200 minutes = 30 days) # Assuming entries are grouped by minute, 30 days = 43200 entries max time_series = [] + endpoint_totals = defaultdict(int) # Track tokens per endpoint + async for entry in db.get_latest_time_series(limit=50000): if entry['model'] == model: time_series.append({ + 'endpoint': entry['endpoint'], 'timestamp': entry['timestamp'], 'input_tokens': entry['input_tokens'], 'output_tokens': entry['output_tokens'], 'total_tokens': entry['total_tokens'] }) + # Accumulate total tokens per endpoint + endpoint_totals[entry['endpoint']] += entry['total_tokens'] return { 'model': model, 'input_tokens': token_data['input_tokens'], 'output_tokens': token_data['output_tokens'], 'total_tokens': token_data['total_tokens'], - 'time_series': time_series + 'time_series': time_series, + 'endpoint_distribution': dict(endpoint_totals) } # ------------------------------------------------------------- diff --git a/static/index.html b/static/index.html index 42a7cd7..17e2ce0 100644 --- a/static/index.html +++ b/static/index.html @@ -232,6 +232,12 @@ height: 300px; margin-top: 1rem; } + .pie-chart-container { + position: relative; + height: 250px; + margin-top: 1rem; + max-width: 400px; + } @@ -376,23 +382,32 @@ function renderTimeSeriesChart(timeSeriesData, chart, minutes) { return; } - // Filter data based on selected timeframe - const now = new Date(); - const cutoffTime = now.getTime() - (minutes * 60 * 1000); + // Filter data based on selected timeframe (use UTC for consistency) + const now = Date.now(); + const cutoffTime = now - (minutes * 60 * 1000); // Group data by hour for better visualization const groupedData = {}; timeSeriesData.forEach(item => { - const timestamp = item.timestamp * 1000; - if (timestamp >= cutoffTime) { - const date = new Date(timestamp); - // Group by hour (local time) - const hourStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + // Database stores UTC timestamps, multiply by 1000 to get milliseconds + const timestampMs = item.timestamp * 1000; + + if (timestampMs >= cutoffTime) { + // Convert UTC timestamp to local time for display + const date = new Date(timestampMs); + // Group by hour and minute in local time + const hourStr = date.toLocaleString([], { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); if (!groupedData[hourStr]) { groupedData[hourStr] = { input: 0, - output: 0 + output: 0, + timestamp: timestampMs // Keep for sorting }; } groupedData[hourStr].input += item.input_tokens || 0; @@ -400,10 +415,11 @@ function renderTimeSeriesChart(timeSeriesData, chart, minutes) { } }); - // Convert to arrays for chart - const labels = Object.keys(groupedData).sort(); - const inputData = labels.map(hour => groupedData[hour].input); - const outputData = labels.map(hour => groupedData[hour].output); + // Convert to arrays for chart, sorted by timestamp + const sortedEntries = Object.entries(groupedData).sort((a, b) => a[1].timestamp - b[1].timestamp); + const labels = sortedEntries.map(([label]) => label); + const inputData = sortedEntries.map(([, data]) => data.input); + const outputData = sortedEntries.map(([, data]) => data.output); console.log('Chart updated with', labels.length, 'data points'); @@ -770,6 +786,10 @@ function renderTimeSeriesChart(timeSeriesData, chart, minutes) {

Input tokens: ${data.input_tokens}

Output tokens: ${data.output_tokens}

Total tokens: ${data.total_tokens}

+

Endpoint Distribution

+
+ +

Usage Over Time

@@ -783,8 +803,8 @@ function renderTimeSeriesChart(timeSeriesData, chart, minutes) { `; document.getElementById("stats-modal").style.display = "flex"; - // Initialise the chart (ensures fresh canvas and chart instance) - initStatsChart(data.time_series); + // Initialise the charts (time-series + pie chart) + initStatsChart(data.time_series, data.endpoint_distribution); } catch (err) { console.error(err); alert(`Could not load model stats: ${err.message}`); @@ -792,8 +812,9 @@ function renderTimeSeriesChart(timeSeriesData, chart, minutes) { }); /* ---------- Helper to initialise or refresh the stats chart ---------- */ -function initStatsChart(timeSeriesData) { +function initStatsChart(timeSeriesData, endpointDistribution) { console.log('initStatsChart called with payload:', timeSeriesData); + console.log('Endpoint distribution:', endpointDistribution); // Destroy any existing chart instance if (statsChart) { statsChart.destroy(); @@ -856,6 +877,57 @@ function initStatsChart(timeSeriesData) { renderTimeSeriesChart(rawTimeSeries, statsChart, minutes); }); }); + + // Create endpoint distribution pie chart + if (endpointDistribution && Object.keys(endpointDistribution).length > 0) { + const pieCanvas = document.getElementById('endpoint-pie-chart'); + const pieCtx = pieCanvas.getContext('2d'); + + const endpoints = Object.keys(endpointDistribution); + const tokenCounts = Object.values(endpointDistribution); + const colors = endpoints.map(ep => getColor(ep)); + + new Chart(pieCtx, { + type: 'pie', + data: { + labels: endpoints, + datasets: [{ + data: tokenCounts, + backgroundColor: colors, + borderWidth: 1, + borderColor: '#fff' + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'right', + labels: { + boxWidth: 12, + font: { size: 11 } + } + }, + title: { + display: true, + text: 'Total Tokens per Endpoint' + }, + tooltip: { + callbacks: { + label: function(context) { + const label = context.label || ''; + const value = context.parsed || 0; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = ((value / total) * 100).toFixed(1); + return `${label}: ${value.toLocaleString()} tokens (${percentage}%)`; + } + } + } + } + } + }); + } } /* stats modal close */ // The close handler is already attached during initial page load.