diff --git a/frontend/src/components/usage/UsageBillingCell.vue b/frontend/src/components/usage/UsageBillingCell.vue
index b162ce11c..a4aa2a5ab 100644
--- a/frontend/src/components/usage/UsageBillingCell.vue
+++ b/frontend/src/components/usage/UsageBillingCell.vue
@@ -63,7 +63,16 @@ function itemValueClass(tone?: string, accent?: boolean) {
{{ usageBillingText(record) }}
-
diff --git a/frontend/src/components/usage/UsagePerformanceCell.vue b/frontend/src/components/usage/UsagePerformanceCell.vue
new file mode 100644
index 000000000..ad3ca5459
--- /dev/null
+++ b/frontend/src/components/usage/UsagePerformanceCell.vue
@@ -0,0 +1,22 @@
+
+
+
+
+ {{ performance.throughputDisplay }}
+
+ TTFT
+ {{ performance.firstTokenDisplay }}
+
+
+
diff --git a/frontend/src/components/usage/UsageRecordsTable.vue b/frontend/src/components/usage/UsageRecordsTable.vue
index 14f9248d1..9e5423bdf 100644
--- a/frontend/src/components/usage/UsageRecordsTable.vue
+++ b/frontend/src/components/usage/UsageRecordsTable.vue
@@ -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(
+
+
+
+
diff --git a/frontend/src/components/usage/shared/columns.ts b/frontend/src/components/usage/shared/columns.ts
index 1c03063ee..36f171302 100644
--- a/frontend/src/components/usage/shared/columns.ts
+++ b/frontend/src/components/usage/shared/columns.ts
@@ -28,6 +28,7 @@ export const usageRecordColumns = defineTableColumns([
{ 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' },
diff --git a/frontend/src/components/usage/shared/presenter.ts b/frontend/src/components/usage/shared/presenter.ts
index 08935d56f..ff57630f8 100644
--- a/frontend/src/components/usage/shared/presenter.ts
+++ b/frontend/src/components/usage/shared/presenter.ts
@@ -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
+
+export type UsagePerformanceRecord = UsageLatencyRecord & {
+ tokenDetails: Pick | 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) {
+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) {
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)
+}
diff --git a/frontend/src/views/key-usage/components/KeyUsageRecords.vue b/frontend/src/views/key-usage/components/KeyUsageRecords.vue
index 8ef1e24c0..5bfa01f40 100644
--- a/frontend/src/views/key-usage/components/KeyUsageRecords.vue
+++ b/frontend/src/views/key-usage/components/KeyUsageRecords.vue
@@ -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([
{ 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([
+
+
+
{{ row.statusCode ?? '错误' }}