mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
feat(channel-monitor-v2): 统一展示成功率、首 Token、每秒 Token 与缓存率
矩阵摘要列补齐每秒 Token(TPM÷60)与缓存率;KPI/模型表/用户榜同步为 成功率·首 Token·每秒 Token·缓存率,吞吐相关列仍受 hide_throughput 控制。
This commit is contained in:
@@ -36,10 +36,15 @@
|
||||
@wheel="onMatrixWheel"
|
||||
>
|
||||
<div class="matrix-table w-full" :style="tableStyle">
|
||||
<div class="matrix-header matrix-row sticky top-0 z-[3] bg-gray-50 text-[10px] font-semibold uppercase tracking-wide text-gray-500 dark:bg-dark-900 dark:text-gray-400">
|
||||
<div
|
||||
class="matrix-header matrix-row sticky top-0 z-[3] bg-gray-50 text-[10px] font-semibold uppercase tracking-wide text-gray-500 dark:bg-dark-900 dark:text-gray-400"
|
||||
:class="showThroughput ? 'matrix-row--with-tps' : ''"
|
||||
>
|
||||
<span>{{ t('channelMonitorV2.matrix.dimension') }}</span>
|
||||
<span>{{ t('channelMonitorV2.metrics.successRate') }}</span>
|
||||
<span>{{ t('channelMonitorV2.metrics.ttft') }}</span>
|
||||
<span v-if="showThroughput">{{ t('channelMonitorV2.metrics.tps') }}</span>
|
||||
<span>{{ t('channelMonitorV2.metrics.cacheRate') }}</span>
|
||||
<span class="pulse-axis flex justify-between gap-3">
|
||||
<i class="not-italic">{{ axisStart }}</i>
|
||||
<i class="not-italic">{{ axisEnd }}</i>
|
||||
@@ -49,6 +54,7 @@
|
||||
v-for="entry in alignedRows"
|
||||
:key="rowKey(entry.row)"
|
||||
class="matrix-row border-b border-gray-100/80 dark:border-dark-700/60"
|
||||
:class="showThroughput ? 'matrix-row--with-tps' : ''"
|
||||
>
|
||||
<div class="dimension-cell flex min-w-0 items-center gap-2 bg-white dark:bg-dark-800" :title="rowLabel(entry.row)">
|
||||
<span :class="['status-dot', cellClass(entry.row.health, entry.row.metrics.request_count)]"></span>
|
||||
@@ -63,6 +69,18 @@
|
||||
>
|
||||
{{ formatMs(entry.row.metrics.ttft.p50_ms) }}
|
||||
</strong>
|
||||
<strong
|
||||
v-if="showThroughput"
|
||||
class="summary-value bg-white text-xs font-medium tabular-nums text-gray-600 dark:bg-dark-800 dark:text-gray-300"
|
||||
:title="exactTps(entry.row.metrics.tpm)"
|
||||
>
|
||||
{{ formatTps(entry.row.metrics.tpm) }}
|
||||
</strong>
|
||||
<strong
|
||||
class="summary-value bg-white text-xs font-medium tabular-nums text-gray-600 dark:bg-dark-800 dark:text-gray-300"
|
||||
>
|
||||
{{ formatPercent(entry.row.metrics.cache_rate) }}
|
||||
</strong>
|
||||
<div class="pulse-track grid items-stretch" :style="pulseStyle">
|
||||
<span
|
||||
v-for="slot in entry.slots"
|
||||
@@ -87,12 +105,12 @@
|
||||
<span class="pulse-tooltip-line pulse-tooltip-title">{{ formatBucketRange(slot.start) }}</span>
|
||||
<span class="pulse-tooltip-line">{{ t('channelMonitorV2.matrix.scoreLine', { score: formatScore(slot.bucket.health) }) }}</span>
|
||||
<span class="pulse-tooltip-line">{{ t('channelMonitorV2.metrics.successRateValue', { value: successRate(slot.bucket.metrics) }) }}</span>
|
||||
<span class="pulse-tooltip-line">{{ t('channelMonitorV2.metrics.ttftValue', { value: latencyPrivacy(slot.bucket.metrics.ttft) }) }}</span>
|
||||
<span v-if="showThroughput" class="pulse-tooltip-line">{{ t('channelMonitorV2.metrics.tpsValue', { value: formatTps(slot.bucket.metrics.tpm) }) }}</span>
|
||||
<span class="pulse-tooltip-line">{{ t('channelMonitorV2.metrics.cacheRateValue', { value: formatPercent(slot.bucket.metrics.cache_rate) }) }}</span>
|
||||
<span class="pulse-tooltip-line">{{ t('channelMonitorV2.metrics.errorRateValue', { value: formatPercent(slot.bucket.metrics.error_rate) }) }}</span>
|
||||
<span v-if="showThroughput" class="pulse-tooltip-line">{{ t('channelMonitorV2.metrics.rpmValue', { value: formatRate(slot.bucket.metrics.rpm) }) }}</span>
|
||||
<span v-if="showThroughput" class="pulse-tooltip-line">{{ t('channelMonitorV2.metrics.tpmValue', { value: formatRate(slot.bucket.metrics.tpm) }) }}</span>
|
||||
<span class="pulse-tooltip-line">{{ t('channelMonitorV2.metrics.ttftValue', { value: latencyPrivacy(slot.bucket.metrics.ttft) }) }}</span>
|
||||
<span class="pulse-tooltip-line">{{ t('channelMonitorV2.metrics.durationValue', { value: latencyPrivacy(slot.bucket.metrics.duration) }) }}</span>
|
||||
<span class="pulse-tooltip-line">{{ t('channelMonitorV2.metrics.cacheRateValue', { value: formatPercent(slot.bucket.metrics.cache_rate) }) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="pulse-tooltip-line pulse-tooltip-title">{{ formatBucketRange(slot.start) }}</span>
|
||||
@@ -165,6 +183,8 @@ import {
|
||||
formatMonitorPercent,
|
||||
formatMonitorSuccessRateFromError,
|
||||
formatMonitorThroughput,
|
||||
formatMonitorTokensPerSecond,
|
||||
tokensPerSecondFromTpm,
|
||||
healthModeScore,
|
||||
healthScoreClass,
|
||||
} from '@/features/channel-monitor-v2/monitorFormat'
|
||||
@@ -347,19 +367,19 @@ function bucketTooltipLines(bucket: MonitorMatrixBucket): string[] {
|
||||
formatBucketRange(bucket.bucket_start),
|
||||
t('channelMonitorV2.matrix.scoreLine', { score: formatScore(bucket.health) }),
|
||||
t('channelMonitorV2.metrics.successRateValue', { value: successRate(metrics) }),
|
||||
t('channelMonitorV2.metrics.errorRateValue', { value: formatPercent(metrics.error_rate) }),
|
||||
t('channelMonitorV2.metrics.ttftValue', { value: latencyPrivacy(metrics.ttft) }),
|
||||
]
|
||||
if (props.showThroughput) {
|
||||
lines.push(
|
||||
t('channelMonitorV2.metrics.rpmValue', { value: formatRate(metrics.rpm) }),
|
||||
t('channelMonitorV2.metrics.tpmValue', { value: formatRate(metrics.tpm) }),
|
||||
)
|
||||
lines.push(t('channelMonitorV2.metrics.tpsValue', { value: formatTps(metrics.tpm) }))
|
||||
}
|
||||
lines.push(
|
||||
t('channelMonitorV2.metrics.ttftValue', { value: latencyPrivacy(metrics.ttft) }),
|
||||
t('channelMonitorV2.metrics.durationValue', { value: latencyPrivacy(metrics.duration) }),
|
||||
t('channelMonitorV2.metrics.cacheRateValue', { value: formatPercent(metrics.cache_rate) }),
|
||||
t('channelMonitorV2.metrics.errorRateValue', { value: formatPercent(metrics.error_rate) }),
|
||||
)
|
||||
if (props.showThroughput) {
|
||||
lines.push(t('channelMonitorV2.metrics.rpmValue', { value: formatRate(metrics.rpm) }))
|
||||
}
|
||||
lines.push(t('channelMonitorV2.metrics.durationValue', { value: latencyPrivacy(metrics.duration) }))
|
||||
return lines
|
||||
}
|
||||
function emptyTooltipLines(start: string): string[] {
|
||||
@@ -406,6 +426,15 @@ function formatRate(value: number) {
|
||||
return formatMonitorThroughput(value)
|
||||
}
|
||||
|
||||
function formatTps(tpm: number | null | undefined) {
|
||||
return formatMonitorTokensPerSecond(tpm)
|
||||
}
|
||||
|
||||
function exactTps(tpm: number | null | undefined) {
|
||||
const tps = tokensPerSecondFromTpm(tpm)
|
||||
return Intl.NumberFormat(locale.value || undefined, { maximumFractionDigits: 3 }).format(tps)
|
||||
}
|
||||
|
||||
function formatMs(value: number | null) {
|
||||
return formatMonitorMs(value)
|
||||
}
|
||||
@@ -427,18 +456,28 @@ function formatBucketRange(value: string) {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* dimension | success | ttft | cache | pulse */
|
||||
.matrix-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 1.25fr) minmax(52px, 0.36fr) minmax(62px, 0.42fr) minmax(120px, 3fr);
|
||||
grid-template-columns:
|
||||
minmax(120px, 1.2fr)
|
||||
minmax(52px, 0.34fr)
|
||||
minmax(58px, 0.36fr)
|
||||
minmax(52px, 0.34fr)
|
||||
minmax(120px, 2.8fr);
|
||||
align-items: center;
|
||||
gap: 0.5rem clamp(0.25rem, 0.8vw, 0.625rem);
|
||||
min-height: 2.25rem;
|
||||
}
|
||||
.matrix-row > :nth-child(2) {
|
||||
left: 0;
|
||||
}
|
||||
.matrix-row > :nth-child(3) {
|
||||
left: 0;
|
||||
/* + tokens/s column when throughput visible */
|
||||
.matrix-row--with-tps {
|
||||
grid-template-columns:
|
||||
minmax(110px, 1.15fr)
|
||||
minmax(48px, 0.3fr)
|
||||
minmax(54px, 0.32fr)
|
||||
minmax(58px, 0.36fr)
|
||||
minmax(48px, 0.3fr)
|
||||
minmax(120px, 2.6fr);
|
||||
}
|
||||
.matrix-table,
|
||||
.pulse-track {
|
||||
|
||||
@@ -8,11 +8,14 @@ const i18nT = (key: string, params?: Record<string, unknown>) => {
|
||||
'channelMonitorV2.matrix.dimension': '维度',
|
||||
'channelMonitorV2.metrics.successRate': '成功率',
|
||||
'channelMonitorV2.metrics.ttft': '首 Token',
|
||||
'channelMonitorV2.metrics.tps': '每秒 Token',
|
||||
'channelMonitorV2.metrics.cacheRate': '缓存率',
|
||||
'channelMonitorV2.matrix.scoreLine': '评分 {score}',
|
||||
'channelMonitorV2.metrics.successRateValue': '成功率 {value}',
|
||||
'channelMonitorV2.metrics.errorRateValue': '错误率 {value}',
|
||||
'channelMonitorV2.metrics.rpmValue': 'RPM {value}',
|
||||
'channelMonitorV2.metrics.tpmValue': 'TPM {value}',
|
||||
'channelMonitorV2.metrics.tpsValue': '每秒 Token {value}',
|
||||
'channelMonitorV2.metrics.ttftValue': '首 Token {value}',
|
||||
'channelMonitorV2.metrics.durationValue': '时长 {value}',
|
||||
'channelMonitorV2.metrics.cacheRateValue': '缓存率 {value}',
|
||||
@@ -113,12 +116,19 @@ describe('RelayPulseMatrix', () => {
|
||||
const tip = cells[0].text()
|
||||
expect(tip).toContain('成功率')
|
||||
expect(tip).toContain('90.0%')
|
||||
expect(tip).toContain('RPM')
|
||||
expect(tip).toContain('TPM')
|
||||
expect(tip).toContain('首 Token')
|
||||
expect(tip).toContain('每秒 Token')
|
||||
expect(tip).toContain('缓存率')
|
||||
expect(tip).toContain('RPM')
|
||||
expect(tip).not.toContain('请求数')
|
||||
expect(tip).not.toContain('用户错误')
|
||||
expect(tip).not.toContain('上游受影响')
|
||||
// Summary columns: success · ttft · tokens/s · cache
|
||||
const header = wrapper.find('.matrix-header').text()
|
||||
expect(header).toContain('成功率')
|
||||
expect(header).toContain('首 Token')
|
||||
expect(header).toContain('每秒 Token')
|
||||
expect(header).toContain('缓存率')
|
||||
// Multi-band class from score 52 → score5
|
||||
expect(cells[0].classes().some((c) => c.startsWith('health-score'))).toBe(true)
|
||||
// Redacted user payloads may have request_count=0 but still include score.
|
||||
|
||||
@@ -9,13 +9,22 @@ import {
|
||||
formatMonitorSuccessRate,
|
||||
formatMonitorSuccessRateFromError,
|
||||
formatMonitorThroughput,
|
||||
formatMonitorTokensPerSecond,
|
||||
healthScoreClass,
|
||||
healthStateClass,
|
||||
scoreToBand,
|
||||
tokensPerSecondFromTpm,
|
||||
} from '../monitorFormat'
|
||||
import type { MonitorHealth } from '@/api/channelMonitorV2'
|
||||
|
||||
describe('monitorFormat accuracy', () => {
|
||||
it('converts backend TPM (per minute) to tokens/sec for display', () => {
|
||||
expect(tokensPerSecondFromTpm(60)).toBe(1)
|
||||
expect(tokensPerSecondFromTpm(6000)).toBe(100)
|
||||
expect(formatMonitorTokensPerSecond(60)).toBe('1')
|
||||
expect(formatMonitorTokensPerSecond(null)).toBe('0')
|
||||
})
|
||||
|
||||
it('formats request/token counts with compact notation above threshold', () => {
|
||||
expect(formatMonitorNumber(10)).toBe('10')
|
||||
expect(formatMonitorNumber(9999)).toBe('9,999')
|
||||
|
||||
@@ -42,6 +42,19 @@ export function formatMonitorThroughput(value: number | null | undefined): strin
|
||||
return formatCompactNumber(n)
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend stores tokens-per-minute as `tpm`. Convert to tokens-per-second for display.
|
||||
*/
|
||||
export function tokensPerSecondFromTpm(tpm: number | null | undefined): number {
|
||||
if (tpm == null || Number.isNaN(Number(tpm))) return 0
|
||||
return Number(tpm) / 60
|
||||
}
|
||||
|
||||
/** Tokens/sec from backend TPM (per-minute), compact ops formatting. */
|
||||
export function formatMonitorTokensPerSecond(tpm: number | null | undefined): string {
|
||||
return formatMonitorThroughput(tokensPerSecondFromTpm(tpm))
|
||||
}
|
||||
|
||||
|
||||
export function formatMonitorPercent(value: number, locale = monitorIntlLocale()): string {
|
||||
return `${new Intl.NumberFormat(locale, {
|
||||
|
||||
@@ -27,8 +27,27 @@ export default {
|
||||
healthMode: { label: 'Health display', overall: 'Overall', success: 'Error rate', ttft: 'First token', cache: 'Cache rate' },
|
||||
tabs: { aria: 'Detail dimension', models: 'Models', errors: 'Error reasons', users: 'User ranking' },
|
||||
metrics: {
|
||||
rpm: 'RPM', tpm: 'TPM', rpmDetail: 'Request rate', tpmDetail: 'Token rate', errorRate: 'Error rate', ttft: 'First token', ttftP50: 'First token P50', durationP50: 'Duration P50', cacheRate: 'Cache rate', cacheDetail: 'Read cache share', successRate: 'Success rate',
|
||||
successRateValue: 'Success rate {value}', errorRateValue: 'Error rate {value}', rpmValue: 'RPM {value}', tpmValue: 'TPM {value}', ttftValue: 'First token {value}', durationValue: 'Duration {value}', cacheRateValue: 'Cache rate {value}'
|
||||
rpm: 'RPM',
|
||||
tpm: 'TPM',
|
||||
tps: 'Tokens/s',
|
||||
rpmDetail: 'Requests per minute',
|
||||
tpmDetail: 'Tokens per minute',
|
||||
tpsDetail: 'Derived as TPM ÷ 60',
|
||||
errorRate: 'Error rate',
|
||||
ttft: 'First token',
|
||||
ttftP50: 'First token P50',
|
||||
durationP50: 'Duration P50',
|
||||
cacheRate: 'Cache rate',
|
||||
cacheDetail: 'Read cache share',
|
||||
successRate: 'Success rate',
|
||||
successRateValue: 'Success rate {value}',
|
||||
errorRateValue: 'Error rate {value}',
|
||||
rpmValue: 'RPM {value}',
|
||||
tpmValue: 'TPM {value}',
|
||||
tpsValue: 'Tokens/s {value}',
|
||||
ttftValue: 'First token {value}',
|
||||
durationValue: 'Duration {value}',
|
||||
cacheRateValue: 'Cache rate {value}',
|
||||
},
|
||||
table: { platformModel: 'Platform / Model', rank: 'Rank', user: 'User' },
|
||||
empty: { title: 'No data to display', description: 'Try changing the time range or filters' },
|
||||
|
||||
@@ -27,8 +27,27 @@ export default {
|
||||
healthMode: { label: '健康显示', overall: '综合', success: '错误率', ttft: '首 Token', cache: '缓存率' },
|
||||
tabs: { aria: '明细维度', models: '模型', errors: '错误原因', users: '用户排行' },
|
||||
metrics: {
|
||||
rpm: 'RPM', tpm: 'TPM', rpmDetail: '请求速率', tpmDetail: 'Token 速率', errorRate: '错误率', ttft: '首 Token', ttftP50: '首 Token P50', durationP50: '请求时长 P50', cacheRate: '缓存率', cacheDetail: '读缓存占比', successRate: '成功率',
|
||||
successRateValue: '成功率 {value}', errorRateValue: '错误率 {value}', rpmValue: 'RPM {value}', tpmValue: 'TPM {value}', ttftValue: '首 Token {value}', durationValue: '请求时长 {value}', cacheRateValue: '缓存率 {value}'
|
||||
rpm: 'RPM',
|
||||
tpm: 'TPM',
|
||||
tps: '每秒 Token',
|
||||
rpmDetail: '每分钟请求数',
|
||||
tpmDetail: '每分钟 Token 数',
|
||||
tpsDetail: '由 TPM ÷ 60 换算',
|
||||
errorRate: '错误率',
|
||||
ttft: '首 Token',
|
||||
ttftP50: '首 Token P50',
|
||||
durationP50: '请求时长 P50',
|
||||
cacheRate: '缓存率',
|
||||
cacheDetail: '读缓存占比',
|
||||
successRate: '成功率',
|
||||
successRateValue: '成功率 {value}',
|
||||
errorRateValue: '错误率 {value}',
|
||||
rpmValue: 'RPM {value}',
|
||||
tpmValue: 'TPM {value}',
|
||||
tpsValue: '每秒 Token {value}',
|
||||
ttftValue: '首 Token {value}',
|
||||
durationValue: '请求时长 {value}',
|
||||
cacheRateValue: '缓存率 {value}',
|
||||
},
|
||||
table: { platformModel: '平台 / 模型', rank: '排名', user: '用户' },
|
||||
empty: { title: '没有可展示的数据', description: '尝试调整时间范围或筛选条件' },
|
||||
|
||||
@@ -151,31 +151,17 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Overview-first KPI strip (Ops metric nest order) -->
|
||||
<!-- Overview KPI: success · TTFT · tokens/s(optional) · cache · (+ RPM when throughput visible) -->
|
||||
<section
|
||||
v-if="snapshot"
|
||||
class="grid grid-cols-2 gap-3 sm:grid-cols-3"
|
||||
:class="showThroughput ? 'xl:grid-cols-6' : 'xl:grid-cols-4'"
|
||||
:class="showThroughput ? 'xl:grid-cols-5' : 'xl:grid-cols-4'"
|
||||
:aria-label="t('channelMonitorV2.summaryAria')"
|
||||
>
|
||||
<MetricCell
|
||||
v-if="showThroughput"
|
||||
:label="t('channelMonitorV2.metrics.rpm')"
|
||||
:value="formatRate(snapshot.metrics.rpm)"
|
||||
:detail="t('channelMonitorV2.metrics.rpmDetail')"
|
||||
:title="exactRate(snapshot.metrics.rpm)"
|
||||
/>
|
||||
<MetricCell
|
||||
v-if="showThroughput"
|
||||
:label="t('channelMonitorV2.metrics.tpm')"
|
||||
:value="formatRate(snapshot.metrics.tpm)"
|
||||
:detail="t('channelMonitorV2.metrics.tpmDetail')"
|
||||
:title="exactRate(snapshot.metrics.tpm)"
|
||||
/>
|
||||
<MetricCell
|
||||
:label="t('channelMonitorV2.metrics.errorRate')"
|
||||
:value="formatPercent(snapshot.metrics.error_rate)"
|
||||
:detail="t('channelMonitorV2.metrics.successRateValue', { value: formatPercent(1 - snapshot.metrics.error_rate) })"
|
||||
:label="t('channelMonitorV2.metrics.successRate')"
|
||||
:value="formatPercent(1 - snapshot.metrics.error_rate)"
|
||||
:detail="t('channelMonitorV2.metrics.errorRateValue', { value: formatPercent(snapshot.metrics.error_rate) })"
|
||||
:state="snapshot.health.error_rate"
|
||||
/>
|
||||
<MetricCell
|
||||
@@ -186,10 +172,11 @@
|
||||
:state="snapshot.health.ttft"
|
||||
/>
|
||||
<MetricCell
|
||||
:label="t('channelMonitorV2.metrics.durationP50')"
|
||||
:value="formatMs(snapshot.metrics.duration.p50_ms)"
|
||||
:detail="latencyKpiSecondary(snapshot.metrics.duration)"
|
||||
:title="latencyDetail(snapshot.metrics.duration)"
|
||||
v-if="showThroughput"
|
||||
:label="t('channelMonitorV2.metrics.tps')"
|
||||
:value="formatTps(snapshot.metrics.tpm)"
|
||||
:detail="t('channelMonitorV2.metrics.tpsDetail')"
|
||||
:title="exactTps(snapshot.metrics.tpm)"
|
||||
/>
|
||||
<MetricCell
|
||||
:label="t('channelMonitorV2.metrics.cacheRate')"
|
||||
@@ -197,15 +184,22 @@
|
||||
:detail="t('channelMonitorV2.metrics.cacheDetail')"
|
||||
:state="snapshot.health.cache || snapshot.health.overall"
|
||||
/>
|
||||
<MetricCell
|
||||
v-if="showThroughput"
|
||||
:label="t('channelMonitorV2.metrics.rpm')"
|
||||
:value="formatRate(snapshot.metrics.rpm)"
|
||||
:detail="t('channelMonitorV2.metrics.rpmDetail')"
|
||||
:title="exactRate(snapshot.metrics.rpm)"
|
||||
/>
|
||||
</section>
|
||||
<section
|
||||
v-else-if="loading"
|
||||
class="grid grid-cols-2 gap-3 sm:grid-cols-3"
|
||||
:class="showThroughput ? 'xl:grid-cols-6' : 'xl:grid-cols-4'"
|
||||
:class="showThroughput ? 'xl:grid-cols-5' : 'xl:grid-cols-4'"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
v-for="i in (showThroughput ? 6 : 4)"
|
||||
v-for="i in (showThroughput ? 5 : 4)"
|
||||
:key="i"
|
||||
class="h-24 animate-pulse rounded-2xl bg-gray-50 dark:bg-dark-900/30"
|
||||
/>
|
||||
@@ -256,12 +250,11 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('channelMonitorV2.table.platformModel') }}</th>
|
||||
<th v-if="showThroughput">{{ t('channelMonitorV2.metrics.rpm') }}</th>
|
||||
<th v-if="showThroughput">{{ t('channelMonitorV2.metrics.tpm') }}</th>
|
||||
<th>{{ t('channelMonitorV2.metrics.errorRate') }}</th>
|
||||
<th>{{ t('channelMonitorV2.metrics.successRate') }}</th>
|
||||
<th>{{ t('channelMonitorV2.metrics.ttftP50') }}</th>
|
||||
<th>{{ t('channelMonitorV2.metrics.durationP50') }}</th>
|
||||
<th v-if="showThroughput">{{ t('channelMonitorV2.metrics.tps') }}</th>
|
||||
<th>{{ t('channelMonitorV2.metrics.cacheRate') }}</th>
|
||||
<th v-if="showThroughput">{{ t('channelMonitorV2.metrics.rpm') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -282,21 +275,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="showThroughput">{{ formatRate(row.metrics.rpm) }}</td>
|
||||
<td v-if="showThroughput">{{ formatRate(row.metrics.tpm) }}</td>
|
||||
<td>
|
||||
<span class="block">{{ formatPercent(row.metrics.error_rate) }}</span>
|
||||
<small class="text-xs text-gray-400">{{ t('channelMonitorV2.metrics.successRateValue', { value: formatPercent(1 - row.metrics.error_rate) }) }}</small>
|
||||
<span class="block">{{ formatPercent(1 - row.metrics.error_rate) }}</span>
|
||||
<small class="text-xs text-gray-400">{{ t('channelMonitorV2.metrics.errorRateValue', { value: formatPercent(row.metrics.error_rate) }) }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="block">{{ formatMs(row.metrics.ttft.p50_ms) }}</span>
|
||||
<small class="text-xs text-gray-400">{{ latencyDetail(row.metrics.ttft) }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="block">{{ formatMs(row.metrics.duration.p50_ms) }}</span>
|
||||
<small class="text-xs text-gray-400">{{ latencyDetail(row.metrics.duration) }}</small>
|
||||
</td>
|
||||
<td v-if="showThroughput" :title="exactTps(row.metrics.tpm)">{{ formatTps(row.metrics.tpm) }}</td>
|
||||
<td>{{ formatPercent(row.metrics.cache_rate) }}</td>
|
||||
<td v-if="showThroughput">{{ formatRate(row.metrics.rpm) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -359,12 +348,11 @@
|
||||
<tr>
|
||||
<th class="w-16">{{ t('channelMonitorV2.table.rank') }}</th>
|
||||
<th>{{ t('channelMonitorV2.table.user') }}</th>
|
||||
<th v-if="showThroughput">{{ t('channelMonitorV2.metrics.rpm') }}</th>
|
||||
<th v-if="showThroughput">{{ t('channelMonitorV2.metrics.tpm') }}</th>
|
||||
<th>{{ t('channelMonitorV2.metrics.errorRate') }}</th>
|
||||
<th>{{ t('channelMonitorV2.metrics.successRate') }}</th>
|
||||
<th>{{ t('channelMonitorV2.metrics.ttftP50') }}</th>
|
||||
<th>{{ t('channelMonitorV2.metrics.durationP50') }}</th>
|
||||
<th v-if="showThroughput">{{ t('channelMonitorV2.metrics.tps') }}</th>
|
||||
<th>{{ t('channelMonitorV2.metrics.cacheRate') }}</th>
|
||||
<th v-if="showThroughput">{{ t('channelMonitorV2.metrics.rpm') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -388,21 +376,17 @@
|
||||
>{{ t('channelMonitorV2.currentUser') }}</span>
|
||||
</strong>
|
||||
</td>
|
||||
<td v-if="showThroughput">{{ formatRate(row.metrics.rpm) }}</td>
|
||||
<td v-if="showThroughput">{{ formatRate(row.metrics.tpm) }}</td>
|
||||
<td>
|
||||
<span class="block">{{ formatPercent(row.metrics.error_rate) }}</span>
|
||||
<small class="text-xs text-gray-400">{{ t('channelMonitorV2.metrics.successRateValue', { value: formatPercent(1 - row.metrics.error_rate) }) }}</small>
|
||||
<span class="block">{{ formatPercent(1 - row.metrics.error_rate) }}</span>
|
||||
<small class="text-xs text-gray-400">{{ t('channelMonitorV2.metrics.errorRateValue', { value: formatPercent(row.metrics.error_rate) }) }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="block">{{ formatMs(row.metrics.ttft.p50_ms) }}</span>
|
||||
<small class="text-xs text-gray-400">{{ latencyDetail(row.metrics.ttft) }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="block">{{ formatMs(row.metrics.duration.p50_ms) }}</span>
|
||||
<small class="text-xs text-gray-400">{{ latencyDetail(row.metrics.duration) }}</small>
|
||||
</td>
|
||||
<td v-if="showThroughput" :title="exactTps(row.metrics.tpm)">{{ formatTps(row.metrics.tpm) }}</td>
|
||||
<td>{{ formatPercent(row.metrics.cache_rate) }}</td>
|
||||
<td v-if="showThroughput">{{ formatRate(row.metrics.rpm) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -456,6 +440,8 @@ import {
|
||||
formatMonitorMs,
|
||||
formatMonitorPercent,
|
||||
formatMonitorThroughput,
|
||||
formatMonitorTokensPerSecond,
|
||||
tokensPerSecondFromTpm,
|
||||
healthScoreClass,
|
||||
monitorErrorCategoryLabel,
|
||||
} from '@/features/channel-monitor-v2/monitorFormat'
|
||||
@@ -771,6 +757,14 @@ function formatRate(value: number) {
|
||||
function exactRate(value: number) {
|
||||
return Intl.NumberFormat(locale.value || undefined, { maximumFractionDigits: 2 }).format(value || 0)
|
||||
}
|
||||
function formatTps(tpm: number | null | undefined) {
|
||||
return formatMonitorTokensPerSecond(tpm)
|
||||
}
|
||||
function exactTps(tpm: number | null | undefined) {
|
||||
return Intl.NumberFormat(locale.value || undefined, { maximumFractionDigits: 3 }).format(
|
||||
tokensPerSecondFromTpm(tpm),
|
||||
)
|
||||
}
|
||||
function formatPercent(value: number) {
|
||||
return formatMonitorPercent(value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user