feat: add TPS and cache hit rate to usage overview

This commit is contained in:
lzt404 committed 2026-07-28 15:07:30 +08:00
1 parent a912dd473a
commit 3d2a7f357e
4 files changed
+47 -9

No files matched your search

+32 -6
View File
@@ -184,7 +184,9 @@ pub(crate) struct UsageOverview {
total_tokens: u64,
rpm: f64,
tpm: f64,
tps: f64,
average_latency_ms: f64,
cache_hit_rate: f64,
timeline: Vec<UsageTimelinePoint>,
}
@@ -718,6 +720,7 @@ fn is_generated_usage_event(value: &Value) -> bool {
"output_tokens",
"reasoning_tokens",
"cached_tokens",
"cache_tokens",
"cache_read_tokens",
"cache_creation_tokens",
"total_tokens",
@@ -745,13 +748,17 @@ fn normalize_usage_record(value: Value, config: &GuiConfigFile) -> Result<UsageR
.map(|entry| entry.remark.clone())
.unwrap_or_default();
let tokens_object = object.get("tokens").and_then(Value::as_object);
let raw_cache_read_tokens = token_u64(tokens_object, "cache_read_tokens");
let cache_creation_tokens = token_u64(tokens_object, "cache_creation_tokens");
let compatible_cached_tokens = token_u64(tokens_object, "cached_tokens")
.max(token_u64(tokens_object, "cache_tokens"))
.saturating_sub(raw_cache_read_tokens.saturating_add(cache_creation_tokens));
let mut tokens = UsageTokenStats {
input_tokens: token_u64(tokens_object, "input_tokens"),
output_tokens: token_u64(tokens_object, "output_tokens"),
reasoning_tokens: token_u64(tokens_object, "reasoning_tokens"),
cache_read_tokens: token_u64(tokens_object, "cache_read_tokens")
.max(token_u64(tokens_object, "cached_tokens")),
cache_creation_tokens: token_u64(tokens_object, "cache_creation_tokens"),
cache_read_tokens: raw_cache_read_tokens.saturating_add(compatible_cached_tokens),
cache_creation_tokens,
total_tokens: token_u64(tokens_object, "total_tokens"),
};
if tokens.total_tokens == 0 {
@@ -876,6 +883,10 @@ fn load_usage_overview(
COALESCE(SUM(cache_creation_tokens), 0),
COALESCE(SUM(total_tokens), 0),
COALESCE(SUM(latency_ms), 0),
COALESCE(AVG(CASE
WHEN output_tokens > 0 AND latency_ms > 0
THEN CAST(output_tokens AS REAL) * 1000.0 / latency_ms
END), 0.0),
MIN(timestamp_ms),
MAX(timestamp_ms)
FROM usage_events{}
@@ -898,8 +909,9 @@ fn load_usage_overview(
row.get::<_, i64>(7)?,
row.get::<_, i64>(8)?,
row.get::<_, i64>(9)?,
row.get::<_, Option<i64>>(10)?,
row.get::<_, f64>(10)?,
row.get::<_, Option<i64>>(11)?,
row.get::<_, Option<i64>>(12)?,
))
},
)
@@ -954,7 +966,12 @@ fn load_usage_overview(
overview.success_count as f64 * 100.0 / overview.total_requests as f64;
overview.average_latency_ms =
from_sql_i64(summary.9) as f64 / overview.total_requests as f64;
let minutes = query_window_minutes(query, summary.10, summary.11);
overview.tps = summary.10;
if overview.input_tokens > 0 {
overview.cache_hit_rate =
(overview.cache_read_tokens as f64 / overview.input_tokens as f64).min(1.0);
}
let minutes = query_window_minutes(query, summary.11, summary.12);
overview.rpm = overview.total_requests as f64 / minutes;
overview.tpm = overview.total_tokens as f64 / minutes;
}
@@ -1483,7 +1500,13 @@ mod tests {
"api_key": "secret-client-key",
"response_headers": { "authorization": ["secret-upstream-token"] },
"model": "gpt-test",
"tokens": { "input_tokens": 10, "output_tokens": 20 }
"tokens": {
"input_tokens": 10,
"output_tokens": 20,
"cached_tokens": 7,
"cache_read_tokens": 5,
"cache_creation_tokens": 2
}
}),
&config,
)
@@ -1493,6 +1516,7 @@ mod tests {
assert!(!rendered.contains("secret-client-key"));
assert!(!rendered.contains("secret-upstream-token"));
assert_eq!(record.tokens.total_tokens, 30);
assert_eq!(record.tokens.cache_read_tokens, 5);
assert!(!record.api_key_hash.is_empty());
}
@@ -1614,6 +1638,8 @@ mod tests {
assert_eq!(overview.total_requests, 1);
assert_eq!(overview.failure_count, 1);
assert_eq!(overview.tps, 200.0);
assert_eq!(overview.cache_hit_rate, 0.2);
assert_eq!(analysis.models[0].key, "gpt-b");
assert_eq!(events.total, 1);
assert_eq!(events.items[0].id, "request-2");
+8
View File
@@ -127,6 +127,10 @@ export const zhCN = {
'usage.stat.successRate': '成功率',
'usage.stat.reasoningMeta': '思考 {tokens} Token',
'usage.stat.averageLatency': '平均延迟',
'usage.stat.tps': 'TPS',
'usage.stat.performanceMeta': '{rpm} RPM · {tpm} TPM · 平均延迟 {latency} ms',
'usage.stat.cacheHitRate': '缓存命中率',
'usage.stat.cacheHitMeta': '命中 {hit} / 输入 {input} Token',
'usage.trend.title': '请求与 Token 趋势',
'usage.trend.description': '按系统本地小时聚合',
'usage.token.title': 'Token 构成',
@@ -942,6 +946,10 @@ export const en: Record<MessageKey, string> = {
'usage.stat.successRate': 'Success Rate',
'usage.stat.reasoningMeta': 'Reasoning {tokens} tokens',
'usage.stat.averageLatency': 'Average Latency',
'usage.stat.tps': 'TPS',
'usage.stat.performanceMeta': '{rpm} RPM · {tpm} TPM · Avg latency {latency} ms',
'usage.stat.cacheHitRate': 'Cache Hit Rate',
'usage.stat.cacheHitMeta': '{hit} hit / {input} input tokens',
'usage.trend.title': 'Request and Token Trend',
'usage.trend.description': 'Grouped by local system hour',
'usage.token.title': 'Token Breakdown',
+4 -1
View File
@@ -35,7 +35,9 @@ type UsageOverview = {
totalTokens: number;
rpm: number;
tpm: number;
tps: number;
averageLatencyMs: number;
cacheHitRate: number;
timeline: TimelinePoint[];
};
@@ -342,7 +344,8 @@ function OverviewView({ overview }: { overview: UsageOverview }) {
{ icon: Activity, label: t('usage.stat.requests'), value: compactNumber(overview.totalRequests), meta: t('usage.stat.requestMeta', { success: overview.successCount, failed: overview.failureCount }) },
{ icon: Sparkles, label: t('usage.stat.tokens'), value: compactNumber(overview.totalTokens), meta: t('usage.stat.tokenMeta', { input: compactNumber(overview.inputTokens), output: compactNumber(overview.outputTokens) }) },
{ icon: ShieldCheck, label: t('usage.stat.successRate'), value: `${overview.successRate.toFixed(1)}%`, meta: t('usage.stat.reasoningMeta', { tokens: compactNumber(overview.reasoningTokens) }) },
{ icon: Clock3, label: t('usage.stat.averageLatency'), value: `${Math.round(overview.averageLatencyMs)} ms`, meta: `${overview.rpm.toFixed(2)} RPM · ${compactNumber(overview.tpm)} TPM` },
{ icon: Clock3, label: t('usage.stat.tps'), value: `${overview.tps.toFixed(1)} TPS`, meta: t('usage.stat.performanceMeta', { rpm: overview.rpm.toFixed(2), tpm: compactNumber(overview.tpm), latency: Math.round(overview.averageLatencyMs) }) },
{ icon: Database, label: t('usage.stat.cacheHitRate'), value: `${(overview.cacheHitRate * 100).toFixed(1)}%`, meta: t('usage.stat.cacheHitMeta', { hit: compactNumber(overview.cacheReadTokens), input: compactNumber(overview.inputTokens) }) },
];
return <div className="usage-overview-layout">
<div className="usage-stat-grid">{cards.map(({ icon: Icon, label, value, meta }) => <article className="panel usage-stat-card" key={label}><span><Icon size={16} />{label}</span><strong>{value}</strong><small>{meta}</small></article>)}</div>
+3 -2
View File
@@ -5218,7 +5218,7 @@ input:focus-visible {
.usage-stat-grid {
display: grid;
grid-column: 1 / -1;
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
}
@@ -5249,8 +5249,9 @@ input:focus-visible {
overflow: hidden;
color: #8a847c;
font-size: 10px;
line-height: 1.45;
text-overflow: ellipsis;
white-space: nowrap;
white-space: normal;
}
.usage-trend-panel,