mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 05:00:19 +08:00
feat(usage): normalize provider stats across usage and cost views
This commit is contained in:
@@ -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),
|
||||
}))
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user