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 @@ + + + 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([ +