diff --git a/i18n.js b/i18n.js index 1290754..ecc07fd 100644 --- a/i18n.js +++ b/i18n.js @@ -409,6 +409,8 @@ const i18n = { 'usage_stats.success_requests': '成功请求', 'usage_stats.failed_requests': '失败请求', 'usage_stats.total_tokens': '总Token数', + 'usage_stats.rpm_30m': 'RPM(近30分钟)', + 'usage_stats.tpm_30m': 'TPM(近30分钟)', 'usage_stats.requests_trend': '请求趋势', 'usage_stats.tokens_trend': 'Token 使用趋势', 'usage_stats.api_details': 'API 详细统计', @@ -957,6 +959,8 @@ const i18n = { 'usage_stats.success_requests': 'Success Requests', 'usage_stats.failed_requests': 'Failed Requests', 'usage_stats.total_tokens': 'Total Tokens', + 'usage_stats.rpm_30m': 'RPM (last 30 min)', + 'usage_stats.tpm_30m': 'TPM (last 30 min)', 'usage_stats.requests_trend': 'Request Trends', 'usage_stats.tokens_trend': 'Token Usage Trends', 'usage_stats.api_details': 'API Details', diff --git a/index.html b/index.html index 4e2e8a3..30890d7 100644 --- a/index.html +++ b/index.html @@ -888,6 +888,26 @@
总Token数
+ +
+
+ +
+
+
0
+
RPM(近30分钟)
+
+
+ +
+
+ +
+
+
0
+
TPM(近30分钟)
+
+
diff --git a/src/modules/usage.js b/src/modules/usage.js index d2bd8c0..d55dac1 100644 --- a/src/modules/usage.js +++ b/src/modules/usage.js @@ -111,7 +111,7 @@ export async function loadUsageStats(usageData = null) { this.updateChartLineSelectors(null); // 清空概览数据 - ['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => { + ['total-requests', 'success-requests', 'failed-requests', 'total-tokens', 'rpm-30m', 'tpm-30m'].forEach(id => { const el = document.getElementById(id); if (el) el.textContent = '-'; }); @@ -139,7 +139,38 @@ export function updateUsageOverview(data) { document.getElementById('total-requests').textContent = safeData.total_requests ?? 0; document.getElementById('success-requests').textContent = safeData.success_count ?? 0; document.getElementById('failed-requests').textContent = safeData.failure_count ?? 0; - document.getElementById('total-tokens').textContent = safeData.total_tokens ?? 0; + const totalTokensValue = safeData.total_tokens ?? 0; + document.getElementById('total-tokens').textContent = this.formatTokensInMillions(totalTokensValue); + + const recentRate = this.calculateRecentPerMinuteRates(30, safeData); + document.getElementById('rpm-30m').textContent = this.formatPerMinuteValue(recentRate.rpm); + document.getElementById('tpm-30m').textContent = this.formatPerMinuteValue(recentRate.tpm); +} + +export function formatTokensInMillions(value) { + const num = Number(value); + if (!Number.isFinite(num)) { + return '0.00M'; + } + return `${(num / 1_000_000).toFixed(2)}M`; +} + +export function formatPerMinuteValue(value) { + const num = Number(value); + if (!Number.isFinite(num)) { + return '0.00'; + } + const abs = Math.abs(num); + if (abs >= 1000) { + return Math.round(num).toLocaleString(); + } + if (abs >= 100) { + return num.toFixed(0); + } + if (abs >= 10) { + return num.toFixed(1); + } + return num.toFixed(2); } export function getModelNamesFromUsage(usage) { @@ -305,6 +336,40 @@ export function collectUsageDetails() { return this.collectUsageDetailsFromUsage(this.currentUsageData); } +export function calculateRecentPerMinuteRates(windowMinutes = 30, usage = null) { + const details = this.collectUsageDetailsFromUsage(usage || this.currentUsageData); + const effectiveWindow = Number.isFinite(windowMinutes) && windowMinutes > 0 + ? windowMinutes + : 30; + + if (!details.length) { + return { rpm: 0, tpm: 0, windowMinutes: effectiveWindow, requestCount: 0, tokenCount: 0 }; + } + + const now = Date.now(); + const windowStart = now - effectiveWindow * 60 * 1000; + let requestCount = 0; + let tokenCount = 0; + + details.forEach(detail => { + const timestamp = Date.parse(detail.timestamp); + if (Number.isNaN(timestamp) || timestamp < windowStart) { + return; + } + requestCount += 1; + tokenCount += this.extractTotalTokens(detail); + }); + + const denominator = effectiveWindow > 0 ? effectiveWindow : 1; + return { + rpm: requestCount / denominator, + tpm: tokenCount / denominator, + windowMinutes: effectiveWindow, + requestCount, + tokenCount + }; +} + export function createHourlyBucketMeta() { const hourMs = 60 * 60 * 1000; const now = new Date(); @@ -690,7 +755,7 @@ export function updateApiStatsTable(data) { modelsHtml = '
'; Object.entries(apiData.models).forEach(([modelName, modelData]) => { const modelRequests = modelData.total_requests ?? 0; - const modelTokens = modelData.total_tokens ?? 0; + const modelTokens = this.formatTokensInMillions(modelData.total_tokens ?? 0); modelsHtml += `
${modelName} @@ -705,7 +770,7 @@ export function updateApiStatsTable(data) { ${endpoint} ${totalRequests} - ${apiData.total_tokens || 0} + ${this.formatTokensInMillions(apiData.total_tokens || 0)} ${successRate !== null ? successRate + '%' : '-'} ${modelsHtml || '-'} @@ -727,11 +792,14 @@ export const usageModule = { getActiveChartLineSelections, collectUsageDetailsFromUsage, collectUsageDetails, + calculateRecentPerMinuteRates, createHourlyBucketMeta, buildHourlySeriesByModel, buildDailySeriesByModel, buildChartDataForMetric, formatHourLabel, + formatTokensInMillions, + formatPerMinuteValue, formatDayLabel, extractTotalTokens, initializeCharts,