feat: Add model performance metrics to dashboard

Add a shared `performance-metrics` feature module for perf metric APIs, DTOs, and formatting, then surface global 24h model performance on the dashboard with cards and a top-model table.

Reuse the shared metrics module from pricing model details, remove duplicated perf API/formatting code from pricing, and add localized labels for the new dashboard performance UI.
This commit is contained in:
t0ng7u
2026-05-08 01:06:44 +08:00
parent a7475a1e67
commit c19d5aa663
19 changed files with 471 additions and 113 deletions
-62
View File
@@ -10,65 +10,3 @@ export async function getPricing(): Promise<PricingData> {
const res = await api.get('/api/pricing')
return res.data
}
export type PerformanceSeriesPoint = {
ts: number
avg_ttft_ms: number
avg_latency_ms: number
success_rate: number
avg_tps: number
}
export type PerformanceGroup = {
group: string
avg_ttft_ms: number
avg_latency_ms: number
success_rate: number
avg_tps: number
series: PerformanceSeriesPoint[]
}
export type PerformanceMetricsData = {
success: boolean
message?: string
data: {
model_name: string
series_schema?: string
groups: PerformanceGroup[]
}
}
export type PerfModelSummary = {
model_name: string
avg_latency_ms: number
success_rate: number
avg_tps: number
request_count: number
}
export type PerfSummaryAllData = {
success: boolean
message?: string
data: {
models: PerfModelSummary[]
}
}
export async function getPerfMetricsSummary(
hours = 24
): Promise<PerfSummaryAllData> {
const res = await api.get(`/api/perf-metrics/summary?hours=${hours}`)
return res.data
}
export async function getPerfMetrics(
modelName: string,
hours = 24
): Promise<PerformanceMetricsData> {
const params = new URLSearchParams({
model: modelName,
hours: String(hours),
})
const res = await api.get(`/api/perf-metrics?${params.toString()}`)
return res.data
}
@@ -1,9 +1,9 @@
import { useEffect, useMemo, useState } from 'react'
import { useMemo, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { getPerfMetricsSummary } from '../api'
import { getPerfMetricsSummary } from '@/features/performance-metrics/api'
import { DEFAULT_PRICING_PAGE_SIZE, DEFAULT_TOKEN_UNIT } from '../constants'
import type { PricingModel, TokenUnit } from '../types'
import { ModelCard } from './model-card'
@@ -24,22 +24,19 @@ export function ModelCardGrid(props: ModelCardGridProps) {
const pageSize = DEFAULT_PRICING_PAGE_SIZE
const tokenUnit = props.tokenUnit ?? DEFAULT_TOKEN_UNIT
const totalPages = Math.max(1, Math.ceil(props.models.length / pageSize))
const currentPage = Math.min(page, totalPages)
const perfQuery = useQuery({
queryKey: ['perf-metrics-summary'],
queryKey: ['perf-metrics-summary', 24],
queryFn: () => getPerfMetricsSummary(24),
staleTime: 60 * 1000,
retry: false,
})
useEffect(() => {
setPage(1)
}, [props.models])
const pagedModels = useMemo(() => {
const start = (page - 1) * pageSize
const start = (currentPage - 1) * pageSize
return props.models.slice(start, start + pageSize)
}, [page, pageSize, props.models])
}, [currentPage, pageSize, props.models])
const perfMap = useMemo(() => {
const map = new Map<string, ModelPerfBadgeData>()
@@ -76,7 +73,7 @@ export function ModelCardGrid(props: ModelCardGridProps) {
<div className='text-muted-foreground flex flex-col items-center justify-between gap-3 border-t px-4 py-3 text-sm sm:flex-row'>
<p className='text-muted-foreground'>
{t('Page {{current}} of {{total}}', {
current: page,
current: currentPage,
total: totalPages,
})}
</p>
@@ -86,7 +83,7 @@ export function ModelCardGrid(props: ModelCardGridProps) {
variant='outline'
size='sm'
onClick={() => setPage((current) => Math.max(1, current - 1))}
disabled={page <= 1}
disabled={currentPage <= 1}
className='gap-1.5'
>
<ChevronLeft className='size-4' />
@@ -99,7 +96,7 @@ export function ModelCardGrid(props: ModelCardGridProps) {
onClick={() =>
setPage((current) => Math.min(totalPages, current + 1))
}
disabled={page >= totalPages}
disabled={currentPage >= totalPages}
className='gap-1.5'
>
{t('Next')}
+1 -2
View File
@@ -14,8 +14,7 @@ import { parseTags } from '../lib/filters'
import { isTokenBasedModel } from '../lib/model-helpers'
import { formatPrice, formatRequestPrice } from '../lib/price'
import type { PricingModel, TokenUnit } from '../types'
import { ModelPerfBadge } from './model-perf-badge'
import type { ModelPerfBadgeData } from './model-perf-badge'
import { ModelPerfBadge, type ModelPerfBadgeData } from './model-perf-badge'
export interface ModelCardProps {
model: PricingModel
@@ -12,13 +12,14 @@ import {
TableRow,
} from '@/components/ui/table'
import { GroupBadge } from '@/components/group-badge'
import { getPerfMetrics, type PerformanceGroup } from '../api'
import { getPerfMetrics } from '@/features/performance-metrics/api'
import {
formatLatency,
formatThroughput,
formatUptimePct,
type UptimeDayPoint,
} from '../lib/mock-stats'
} from '@/features/performance-metrics/lib/format'
import type { PerformanceGroup } from '@/features/performance-metrics/types'
import { type UptimeDayPoint } from '../lib/mock-stats'
import type { PricingModel } from '../types'
import { LatencyTrendChart, UptimeTrendChart } from './model-details-charts'
import { UptimeSparkline } from './model-details-uptime-sparkline'
@@ -142,7 +143,10 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
queryFn: () => getPerfMetrics(props.model.model_name, 24),
staleTime: 60 * 1000,
})
const groups = metricsQuery.data?.data.groups ?? []
const groups = useMemo(
() => metricsQuery.data?.data.groups ?? [],
[metricsQuery.data]
)
const performances = useMemo<PerformanceRow[]>(
() =>
groups.map((group) => ({
@@ -7,11 +7,8 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import {
aggregateUptime,
formatUptimePct,
type UptimeDayPoint,
} from '../lib/mock-stats'
import { formatUptimePct } from '@/features/performance-metrics/lib/format'
import { aggregateUptime, type UptimeDayPoint } from '../lib/mock-stats'
// ---------------------------------------------------------------------------
// Uptime sparkline
@@ -26,7 +26,12 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { CopyButton } from '@/components/copy-button'
import { GroupBadge } from '@/components/group-badge'
import { PublicLayout } from '@/components/layout'
import { getPerfMetrics } from '../api'
import { getPerfMetrics } from '@/features/performance-metrics/api'
import {
formatLatency,
formatThroughput,
formatUptimePct,
} from '@/features/performance-metrics/lib/format'
import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants'
import { usePricingData } from '../hooks/use-pricing-data'
import {
@@ -36,11 +41,6 @@ import {
isDynamicPricingModel,
} from '../lib/dynamic-price'
import { parseTags } from '../lib/filters'
import {
formatLatency,
formatThroughput,
formatUptimePct,
} from '../lib/mock-stats'
import { getAvailableGroups, isTokenBasedModel } from '../lib/model-helpers'
import { inferModelMetadata } from '../lib/model-metadata'
import { formatFixedPrice, formatGroupPrice } from '../lib/price'
@@ -1,7 +1,10 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { formatLatency, formatThroughput } from '../lib/mock-stats'
import {
formatLatency,
formatThroughput,
} from '@/features/performance-metrics/lib/format'
export type ModelPerfBadgeData = {
avg_latency_ms: number
-20
View File
@@ -464,26 +464,6 @@ export function aggregateUptime(points: UptimeDayPoint[]): {
}
}
/** Format throughput for display: "0" → "—". */
export function formatThroughput(tps: number): string {
if (tps <= 0) return '—'
if (tps >= 1_000) return `${(tps / 1_000).toFixed(1)}K t/s`
return `${tps.toFixed(tps < 10 ? 2 : 1)} t/s`
}
/** Format latency in ms with proper unit selection. */
export function formatLatency(ms: number): string {
if (!Number.isFinite(ms) || ms <= 0) return '—'
if (ms >= 1_000) return `${(ms / 1_000).toFixed(2)}s`
return `${Math.round(ms)}ms`
}
/** Format uptime percentage with 2 decimal places. */
export function formatUptimePct(pct: number): string {
if (!Number.isFinite(pct)) return '—'
return `${pct.toFixed(2)}%`
}
/** Compact integer formatter for token counts in apps tab. */
export function formatTokenVolume(n: number): string {
if (!Number.isFinite(n) || n <= 0) return '0'