feat: add TPS and cache hit rate to usage overview
This commit is contained in:
1 parent
a912dd473a
commit
3d2a7f357e
4 files changed
+47
-9
No files matched your search
+32
-6
@@ -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");
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user