mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 11:10:49 +08:00
feat: add RPM and TPM metrics for the last 30 minutes with internationalization support
This commit is contained in:
4
i18n.js
4
i18n.js
@@ -409,6 +409,8 @@ const i18n = {
|
|||||||
'usage_stats.success_requests': '成功请求',
|
'usage_stats.success_requests': '成功请求',
|
||||||
'usage_stats.failed_requests': '失败请求',
|
'usage_stats.failed_requests': '失败请求',
|
||||||
'usage_stats.total_tokens': '总Token数',
|
'usage_stats.total_tokens': '总Token数',
|
||||||
|
'usage_stats.rpm_30m': 'RPM(近30分钟)',
|
||||||
|
'usage_stats.tpm_30m': 'TPM(近30分钟)',
|
||||||
'usage_stats.requests_trend': '请求趋势',
|
'usage_stats.requests_trend': '请求趋势',
|
||||||
'usage_stats.tokens_trend': 'Token 使用趋势',
|
'usage_stats.tokens_trend': 'Token 使用趋势',
|
||||||
'usage_stats.api_details': 'API 详细统计',
|
'usage_stats.api_details': 'API 详细统计',
|
||||||
@@ -957,6 +959,8 @@ const i18n = {
|
|||||||
'usage_stats.success_requests': 'Success Requests',
|
'usage_stats.success_requests': 'Success Requests',
|
||||||
'usage_stats.failed_requests': 'Failed Requests',
|
'usage_stats.failed_requests': 'Failed Requests',
|
||||||
'usage_stats.total_tokens': 'Total Tokens',
|
'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.requests_trend': 'Request Trends',
|
||||||
'usage_stats.tokens_trend': 'Token Usage Trends',
|
'usage_stats.tokens_trend': 'Token Usage Trends',
|
||||||
'usage_stats.api_details': 'API Details',
|
'usage_stats.api_details': 'API Details',
|
||||||
|
|||||||
20
index.html
20
index.html
@@ -888,6 +888,26 @@
|
|||||||
<div class="stat-label" data-i18n="usage_stats.total_tokens">总Token数</div>
|
<div class="stat-label" data-i18n="usage_stats.total_tokens">总Token数</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-gauge-high"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number" id="rpm-30m">0</div>
|
||||||
|
<div class="stat-label" data-i18n="usage_stats.rpm_30m">RPM(近30分钟)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-stopwatch"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-number" id="tpm-30m">0</div>
|
||||||
|
<div class="stat-label" data-i18n="usage_stats.tpm_30m">TPM(近30分钟)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图表曲线选择 -->
|
<!-- 图表曲线选择 -->
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export async function loadUsageStats(usageData = null) {
|
|||||||
this.updateChartLineSelectors(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);
|
const el = document.getElementById(id);
|
||||||
if (el) el.textContent = '-';
|
if (el) el.textContent = '-';
|
||||||
});
|
});
|
||||||
@@ -139,7 +139,38 @@ export function updateUsageOverview(data) {
|
|||||||
document.getElementById('total-requests').textContent = safeData.total_requests ?? 0;
|
document.getElementById('total-requests').textContent = safeData.total_requests ?? 0;
|
||||||
document.getElementById('success-requests').textContent = safeData.success_count ?? 0;
|
document.getElementById('success-requests').textContent = safeData.success_count ?? 0;
|
||||||
document.getElementById('failed-requests').textContent = safeData.failure_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) {
|
export function getModelNamesFromUsage(usage) {
|
||||||
@@ -305,6 +336,40 @@ export function collectUsageDetails() {
|
|||||||
return this.collectUsageDetailsFromUsage(this.currentUsageData);
|
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() {
|
export function createHourlyBucketMeta() {
|
||||||
const hourMs = 60 * 60 * 1000;
|
const hourMs = 60 * 60 * 1000;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -690,7 +755,7 @@ export function updateApiStatsTable(data) {
|
|||||||
modelsHtml = '<div class="model-details">';
|
modelsHtml = '<div class="model-details">';
|
||||||
Object.entries(apiData.models).forEach(([modelName, modelData]) => {
|
Object.entries(apiData.models).forEach(([modelName, modelData]) => {
|
||||||
const modelRequests = modelData.total_requests ?? 0;
|
const modelRequests = modelData.total_requests ?? 0;
|
||||||
const modelTokens = modelData.total_tokens ?? 0;
|
const modelTokens = this.formatTokensInMillions(modelData.total_tokens ?? 0);
|
||||||
modelsHtml += `
|
modelsHtml += `
|
||||||
<div class="model-item">
|
<div class="model-item">
|
||||||
<span class="model-name">${modelName}</span>
|
<span class="model-name">${modelName}</span>
|
||||||
@@ -705,7 +770,7 @@ export function updateApiStatsTable(data) {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>${endpoint}</td>
|
<td>${endpoint}</td>
|
||||||
<td>${totalRequests}</td>
|
<td>${totalRequests}</td>
|
||||||
<td>${apiData.total_tokens || 0}</td>
|
<td>${this.formatTokensInMillions(apiData.total_tokens || 0)}</td>
|
||||||
<td>${successRate !== null ? successRate + '%' : '-'}</td>
|
<td>${successRate !== null ? successRate + '%' : '-'}</td>
|
||||||
<td>${modelsHtml || '-'}</td>
|
<td>${modelsHtml || '-'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -727,11 +792,14 @@ export const usageModule = {
|
|||||||
getActiveChartLineSelections,
|
getActiveChartLineSelections,
|
||||||
collectUsageDetailsFromUsage,
|
collectUsageDetailsFromUsage,
|
||||||
collectUsageDetails,
|
collectUsageDetails,
|
||||||
|
calculateRecentPerMinuteRates,
|
||||||
createHourlyBucketMeta,
|
createHourlyBucketMeta,
|
||||||
buildHourlySeriesByModel,
|
buildHourlySeriesByModel,
|
||||||
buildDailySeriesByModel,
|
buildDailySeriesByModel,
|
||||||
buildChartDataForMetric,
|
buildChartDataForMetric,
|
||||||
formatHourLabel,
|
formatHourLabel,
|
||||||
|
formatTokensInMillions,
|
||||||
|
formatPerMinuteValue,
|
||||||
formatDayLabel,
|
formatDayLabel,
|
||||||
extractTotalTokens,
|
extractTotalTokens,
|
||||||
initializeCharts,
|
initializeCharts,
|
||||||
|
|||||||
Reference in New Issue
Block a user