feat(usage): normalize provider stats across usage and cost views

This commit is contained in:
zhefox
2026-05-23 22:22:37 +08:00
parent 91cb2bbbcc
commit 9f6fac418e
4 changed files with 104 additions and 29 deletions
@@ -13,6 +13,7 @@ import type {
import { createDefaultStats } from '../types'
import { log } from '@/utils/logger'
import { getErrorStatus } from '@/types/api-error'
import { isUsageProviderVisible, normalizeUsageProviderStats } from '../utils/providerStats'
export interface UseUsageDataOptions {
isAdminPage: Ref<boolean>
@@ -38,11 +39,6 @@ export interface FilterParams {
client_family?: string
}
function isUsageProviderVisible(provider: string | undefined | null): provider is string {
const normalized = provider?.trim().toLowerCase()
return !!normalized && !['unknown', 'unknow', 'pending'].includes(normalized)
}
export function useUsageData(options: UseUsageDataOptions) {
const { isAdminPage } = options
@@ -171,29 +167,8 @@ export function useUsageData(options: UseUsageDataOptions) {
return true
}
const visibleProviderData = providerData.filter(item => isUsageProviderVisible(item.provider))
providerStats.value = visibleProviderData.map(item => ({
providerId: item.provider_id,
providerKey: item.provider_key,
providerIdentitySource: item.provider_identity_source,
provider: item.provider,
requests: item.request_count,
totalTokens: item.total_tokens || 0,
effectiveInputTokens: item.effective_input_tokens || 0,
totalInputContext: item.total_input_context || 0,
outputTokens: item.output_tokens || 0,
cacheReadTokens: item.cache_read_tokens || 0,
cacheCreationTokens: item.cache_creation_tokens || 0,
cacheHitRate: item.cache_hit_rate || 0,
totalCost: item.total_cost,
actualCost: item.actual_cost,
successRate: item.success_rate,
avgResponseTime: item.avg_response_time_ms > 0
? `${(item.avg_response_time_ms / 1000).toFixed(2)}s`
: '-'
}))
availableProviders.value = visibleProviderData.map(item => item.provider).sort()
providerStats.value = normalizeUsageProviderStats(providerData)
availableProviders.value = providerStats.value.map(item => item.provider).sort()
} catch (error) {
if (requestId !== loadStatsRequestId) {
return true
@@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest'
import { isUsageProviderVisible, normalizeUsageProviderStats } from '../providerStats'
describe('usage provider stats normalization', () => {
it('maps admin provider aggregation fields to table fields', () => {
const rows = normalizeUsageProviderStats([
{
provider_id: 'provider-openai',
provider_key: 'provider-openai',
provider_identity_source: 'provider_id',
provider: 'OpenAI',
request_count: 12,
total_tokens: 3456,
effective_input_tokens: 1200,
total_input_context: 1600,
output_tokens: 2256,
cache_read_tokens: 240,
cache_creation_tokens: 60,
cache_hit_rate: 15,
total_cost: 0.123456,
actual_cost: 0.2,
avg_response_time_ms: 1250,
success_rate: 91.67,
error_count: 1,
},
])
expect(rows).toEqual([
{
providerId: 'provider-openai',
providerKey: 'provider-openai',
providerIdentitySource: 'provider_id',
provider: 'OpenAI',
requests: 12,
totalTokens: 3456,
effectiveInputTokens: 1200,
totalInputContext: 1600,
outputTokens: 2256,
cacheReadTokens: 240,
cacheCreationTokens: 60,
cacheHitRate: 15,
totalCost: 0.123456,
actualCost: 0.2,
successRate: 91.67,
avgResponseTime: '1.25s',
},
])
})
it('filters placeholder providers', () => {
expect(isUsageProviderVisible('OpenAI')).toBe(true)
expect(isUsageProviderVisible('unknown')).toBe(false)
expect(isUsageProviderVisible('unknow')).toBe(false)
expect(isUsageProviderVisible('pending')).toBe(false)
expect(isUsageProviderVisible(' ')).toBe(false)
})
})
@@ -0,0 +1,41 @@
import type { UsageByProvider } from '@/api/usage'
import type { ProviderStatsItem } from '../types'
function metricValue(value: number | null | undefined): number {
return typeof value === 'number' && Number.isFinite(value) ? value : 0
}
export function isUsageProviderVisible(provider: string | undefined | null): provider is string {
const normalized = provider?.trim().toLowerCase()
return !!normalized && !['unknown', 'unknow', 'pending'].includes(normalized)
}
export function formatProviderAverageResponseTime(avgResponseTimeMs: number | null | undefined): string {
const value = metricValue(avgResponseTimeMs)
return value > 0 ? `${(value / 1000).toFixed(2)}s` : '-'
}
export function normalizeUsageProviderStats(providerData: UsageByProvider[]): ProviderStatsItem[] {
return providerData
.filter(item => isUsageProviderVisible(item.provider))
.map(item => ({
providerId: item.provider_id,
providerKey: item.provider_key,
providerIdentitySource: item.provider_identity_source,
provider: item.provider,
requests: metricValue(item.request_count),
totalTokens: metricValue(item.total_tokens),
effectiveInputTokens: metricValue(item.effective_input_tokens),
totalInputContext: metricValue(item.total_input_context),
outputTokens: metricValue(item.output_tokens),
cacheReadTokens: metricValue(item.cache_read_tokens),
cacheCreationTokens: metricValue(item.cache_creation_tokens),
cacheHitRate: metricValue(item.cache_hit_rate),
totalCost: metricValue(item.total_cost),
actualCost: typeof item.actual_cost === 'number' && Number.isFinite(item.actual_cost)
? item.actual_cost
: undefined,
successRate: metricValue(item.success_rate),
avgResponseTime: formatProviderAverageResponseTime(item.avg_response_time_ms),
}))
}
+2 -1
View File
@@ -111,6 +111,7 @@ import { adminApi, type CostForecastResponse, type CostSavingsResponse, type Lea
import { usageApi } from '@/api/usage'
import { formatCurrency, formatTokens } from '@/utils/format'
import { getDateRangeFromPeriod } from '@/features/usage/composables'
import { normalizeUsageProviderStats } from '@/features/usage/utils/providerStats'
import type { DateRangeParams } from '@/features/usage/types'
import type { ProviderStatsItem } from '@/features/usage/types'
@@ -196,7 +197,7 @@ async function loadProviderStats() {
limit: 8
})
if (requestId !== providerStatsRequestId) return
providerStats.value = stats
providerStats.value = normalizeUsageProviderStats(stats)
}
async function loadApiKeyLeaderboard() {