feat(channel-monitor-v2): 统一展示成功率、首 Token、每秒 Token 与缓存率

矩阵摘要列补齐每秒 Token(TPM÷60)与缓存率;KPI/模型表/用户榜同步为
成功率·首 Token·每秒 Token·缓存率,吞吐相关列仍受 hide_throughput 控制。
This commit is contained in:
IanShaw027
2026-08-07 11:10:55 +08:00
parent d2d91ff9a8
commit 242f2b78c2
7 changed files with 175 additions and 72 deletions
@@ -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: '尝试调整时间范围或筛选条件' },
+43 -49
View File
@@ -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)
}