feat(usage): add performance column and align Fast indicators (#354)

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-10-06 02:36:41 +08:00
committed by GitHub
co-authored by Zyy Codex
parent 0a47b5efc0
commit c4d8732a73
6 changed files with 71 additions and 15 deletions
@@ -63,7 +63,16 @@ function itemValueClass(tone?: string, accent?: boolean) {
{{ usageBillingText(record) }}
</span>
<div v-if="billing" class="flex flex-col items-center gap-1">
<div v-if="billing" class="flex items-center gap-1">
<span
v-if="billing.serviceTierDisplay === 'Fast'"
class="inline-flex size-4 items-center justify-center rounded-full bg-cp-warning-container text-cp-warning-on-container"
title="Fast 模式"
role="img"
aria-label="Fast 模式"
>
<Zap class="size-3" stroke-width="2.2" aria-hidden="true" />
</span>
<UsageDetailPopover
:title="billing.longContextBillingApplied ? '长上下文计费明细' : '计费明细'"
:trigger-label="billing.longContextBillingApplied ? '查看长上下文计费明细' : '查看费用明细'"
@@ -86,15 +95,6 @@ function itemValueClass(tone?: string, accent?: boolean) {
</div>
</div>
</UsageDetailPopover>
<span
v-if="billing.serviceTierDisplay === 'Fast'"
class="inline-flex size-4 items-center justify-center rounded-full bg-cp-warning-container text-cp-warning-on-container"
title="Fast 加速"
role="img"
aria-label="Fast 加速"
>
<Zap class="size-3" stroke-width="2.2" aria-hidden="true" />
</span>
</div>
</div>
</template>
@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { UsagePerformanceRecord } from './shared/presenter'
import { computed } from 'vue'
import { usagePerformanceDetails } from './shared/presenter'
const props = defineProps<{
record: UsagePerformanceRecord
}>()
const performance = computed(() => usagePerformanceDetails(props.record))
</script>
<template>
<div class="grid justify-items-end gap-y-1.5 whitespace-nowrap font-mono text-cp-sm leading-none font-heavy tabular-nums">
<span class="text-cp-text">{{ performance.throughputDisplay }}</span>
<span class="text-cp-xs text-cp-text-secondary">
<span title="首字时间" class="text-cp-text-quaternary">TTFT</span>
{{ performance.firstTokenDisplay }}
</span>
</div>
</template>
@@ -10,6 +10,7 @@ import UsageBillingCell from '@/components/usage/UsageBillingCell.vue'
import UsageClientIpCell from '@/components/usage/UsageClientIpCell.vue'
import UsageLatencyCell from '@/components/usage/UsageLatencyCell.vue'
import UsageModelCell from '@/components/usage/UsageModelCell.vue'
import UsagePerformanceCell from '@/components/usage/UsagePerformanceCell.vue'
import UsageReasoningEffortCell from '@/components/usage/UsageReasoningEffortCell.vue'
import UsageTokenCell from '@/components/usage/UsageTokenCell.vue'
import UsageTransportBadge from '@/components/usage/UsageTransportBadge.vue'
@@ -134,6 +135,10 @@ withDefaults(
<UsageLatencyCell :record="row" />
</template>
<template #performance="{ row }">
<UsagePerformanceCell :record="row" />
</template>
<template v-if="$slots.actions" #actions="scope">
<slot name="actions" v-bind="scope" />
</template>
@@ -28,6 +28,7 @@ export const usageRecordColumns = defineTableColumns<UsageListRecord>([
{ key: 'tokenDetails', label: 'TOKEN', kind: 'numeric', size: 'xl' },
{ key: 'billing', label: '费用', kind: 'numeric', size: 'xl' },
{ key: 'latency', label: '延迟', kind: 'numeric', size: 'xl' },
{ key: 'performance', label: '性能', kind: 'numeric', size: 'lg' },
{ key: 'createdAtDisplay', label: '时间', kind: 'datetime' },
{ key: 'clientIp', label: 'IP', kind: 'custom', size: '3xl' },
{ key: 'userAgent', label: 'User-Agent', kind: 'custom', size: '4xl' },
@@ -1,8 +1,13 @@
import type { UsageListRecord, UsageRecordDetail } from '@/api'
import { formatDuration } from '@/utils/format'
import { formatCompactNumber, formatDuration } from '@/utils/format'
// 列表与详情使用独立读模型,展示函数只依赖两者的公共字段。
type UsageCommonRecord = UsageListRecord | UsageRecordDetail
type UsageLatencyRecord = Pick<UsageCommonRecord, 'latencyDetails' | 'firstTokenLatencyMs' | 'latencyMs'>
export type UsagePerformanceRecord = UsageLatencyRecord & {
tokenDetails: Pick<UsageListRecord['tokenDetails'], 'outputTokens'> | null
}
export function usageTransportType(transport?: string | null) {
if (transport === 'websocket')
@@ -78,11 +83,25 @@ export function usageModelDisplay(record: UsageCommonRecord) {
return { primary, secondary, routes }
}
export function usageLatencyDetails(record: Pick<UsageCommonRecord, 'latencyDetails' | 'firstTokenLatencyMs' | 'latencyMs'>) {
export function usagePerformanceDetails(record: UsagePerformanceRecord) {
const firstTokenMs = usageFirstTokenMs(record)
const totalMs = durationValue(record.latencyMs)
const outputTokens = record.tokenDetails?.outputTokens
// 与性能统计使用同一吞吐口径,首个生命周期事件不能代替首字
const throughput = typeof outputTokens === 'number' && Number.isFinite(outputTokens) && outputTokens > 0
&& firstTokenMs !== null && totalMs !== null && totalMs > firstTokenMs
? outputTokens * 1000 / (totalMs - firstTokenMs)
: null
return {
throughputDisplay: throughput === null ? '—' : `${formatCompactNumber(throughput)} tok/s`,
firstTokenDisplay: formatDuration(firstTokenMs),
}
}
export function usageLatencyDetails(record: UsageLatencyRecord) {
const latencyDetails = record.latencyDetails
const firstTokenMs = durationValue(
record.firstTokenLatencyMs ?? latencyDetails?.firstTokenMs,
)
const firstTokenMs = usageFirstTokenMs(record)
const firstEventMs = durationValue(latencyDetails?.firstEventMs)
const totalMs = durationValue(record.latencyMs)
const firstReasoningMs = durationValue(latencyDetails?.firstReasoningMs)
@@ -152,3 +171,7 @@ export function usageBillingText(record: Pick<UsageCommonRecord, 'billing'>) {
function durationValue(value: unknown) {
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null
}
function usageFirstTokenMs(record: UsageLatencyRecord) {
return durationValue(record.firstTokenLatencyMs ?? record.latencyDetails?.firstTokenMs)
}
@@ -6,6 +6,7 @@ import { computed } from 'vue'
import UsageBillingCell from '@/components/usage/UsageBillingCell.vue'
import UsageClientIpCell from '@/components/usage/UsageClientIpCell.vue'
import UsageLatencyCell from '@/components/usage/UsageLatencyCell.vue'
import UsagePerformanceCell from '@/components/usage/UsagePerformanceCell.vue'
import UsageTokenCell from '@/components/usage/UsageTokenCell.vue'
import UsageTransportBadge from '@/components/usage/UsageTransportBadge.vue'
@@ -21,6 +22,7 @@ const columns = computed(() => defineTableColumns<KeyUsageRecord>([
{ key: 'tokenDetails', label: 'TOKEN', kind: 'numeric', size: 'xl' },
{ key: 'billing', label: '费用', kind: 'numeric', size: 'xl' },
{ key: 'latency', label: '延迟', kind: 'numeric', size: 'xl' },
{ key: 'performance', label: '性能', kind: 'numeric', size: 'lg' },
...(kind.value === 'error' ? [{ key: 'statusCode', label: '状态', kind: 'status' as const }] : []),
{ key: 'createdAt', label: '时间', kind: 'datetime' },
{ key: 'clientIp', label: 'IP', kind: 'custom', size: '3xl' },
@@ -60,6 +62,9 @@ const columns = computed(() => defineTableColumns<KeyUsageRecord>([
<template #latency="{ row }">
<UsageLatencyCell :record="row" />
</template>
<template #performance="{ row }">
<UsagePerformanceCell :record="row" />
</template>
<template #statusCode="{ row }">
<span class="font-mono text-cp-sm font-bold text-cp-error-text">{{ row.statusCode ?? '错误' }}</span>
</template>