feat(frontend): display upstream model in usage table and distribution charts
Show upstream model mapping (requested -> upstream) in UsageTable with arrow notation. Add requested/upstream/mapping source toggle to ModelDistributionChart with lazy loading — only fetches data when user switches tab, with per-source cache invalidation on filter changes. Include upstream_model column in Excel export and i18n for en/zh.
This commit is contained in:
@@ -25,8 +25,16 @@
|
||||
<span class="text-sm text-gray-900 dark:text-white">{{ row.account?.name || '-' }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-model="{ value }">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
||||
<template #cell-model="{ row }">
|
||||
<div v-if="row.upstream_model && row.upstream_model !== row.model" class="space-y-0.5 text-xs">
|
||||
<div class="break-all font-medium text-gray-900 dark:text-white">
|
||||
{{ row.model }}
|
||||
</div>
|
||||
<div class="break-all text-gray-500 dark:text-gray-400">
|
||||
<span class="mr-0.5">↳</span>{{ row.upstream_model }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="font-medium text-gray-900 dark:text-white">{{ row.model }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-reasoning_effort="{ row }">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="card p-4">
|
||||
<div class="mb-4 flex items-start justify-between gap-3">
|
||||
<div class="mb-4 flex items-center justify-between gap-3">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ title || t('usage.endpointDistribution') }}
|
||||
</h3>
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div
|
||||
v-if="showSourceToggle"
|
||||
class="inline-flex rounded-lg border border-gray-200 bg-gray-50 p-0.5 dark:border-gray-700 dark:bg-dark-800"
|
||||
|
||||
@@ -6,7 +6,42 @@
|
||||
? t('admin.dashboard.modelDistribution')
|
||||
: t('admin.dashboard.spendingRankingTitle') }}
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<div
|
||||
v-if="showSourceToggle"
|
||||
class="inline-flex rounded-lg border border-gray-200 bg-gray-50 p-0.5 dark:border-gray-700 dark:bg-dark-800"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md px-2.5 py-1 text-xs font-medium transition-colors"
|
||||
:class="source === 'requested'
|
||||
? 'bg-white text-gray-900 shadow-sm dark:bg-dark-700 dark:text-white'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
|
||||
@click="emit('update:source', 'requested')"
|
||||
>
|
||||
{{ t('usage.requestedModel') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md px-2.5 py-1 text-xs font-medium transition-colors"
|
||||
:class="source === 'upstream'
|
||||
? 'bg-white text-gray-900 shadow-sm dark:bg-dark-700 dark:text-white'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
|
||||
@click="emit('update:source', 'upstream')"
|
||||
>
|
||||
{{ t('usage.upstreamModel') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md px-2.5 py-1 text-xs font-medium transition-colors"
|
||||
:class="source === 'mapping'
|
||||
? 'bg-white text-gray-900 shadow-sm dark:bg-dark-700 dark:text-white'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
|
||||
@click="emit('update:source', 'mapping')"
|
||||
>
|
||||
{{ t('usage.mapping') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="showMetricToggle"
|
||||
class="inline-flex rounded-lg border border-gray-200 bg-gray-50 p-0.5 dark:border-gray-700 dark:bg-dark-800"
|
||||
@@ -215,9 +250,13 @@ ChartJS.register(ArcElement, Tooltip, Legend)
|
||||
const { t } = useI18n()
|
||||
|
||||
type DistributionMetric = 'tokens' | 'actual_cost'
|
||||
type ModelSource = 'requested' | 'upstream' | 'mapping'
|
||||
type RankingDisplayItem = UserSpendingRankingItem & { isOther?: boolean }
|
||||
const props = withDefaults(defineProps<{
|
||||
modelStats: ModelStat[]
|
||||
upstreamModelStats?: ModelStat[]
|
||||
mappingModelStats?: ModelStat[]
|
||||
source?: ModelSource
|
||||
enableRankingView?: boolean
|
||||
rankingItems?: UserSpendingRankingItem[]
|
||||
rankingTotalActualCost?: number
|
||||
@@ -225,12 +264,16 @@ const props = withDefaults(defineProps<{
|
||||
rankingTotalTokens?: number
|
||||
loading?: boolean
|
||||
metric?: DistributionMetric
|
||||
showSourceToggle?: boolean
|
||||
showMetricToggle?: boolean
|
||||
rankingLoading?: boolean
|
||||
rankingError?: boolean
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}>(), {
|
||||
upstreamModelStats: () => [],
|
||||
mappingModelStats: () => [],
|
||||
source: 'requested',
|
||||
enableRankingView: false,
|
||||
rankingItems: () => [],
|
||||
rankingTotalActualCost: 0,
|
||||
@@ -238,6 +281,7 @@ const props = withDefaults(defineProps<{
|
||||
rankingTotalTokens: 0,
|
||||
loading: false,
|
||||
metric: 'tokens',
|
||||
showSourceToggle: false,
|
||||
showMetricToggle: false,
|
||||
rankingLoading: false,
|
||||
rankingError: false
|
||||
@@ -261,6 +305,7 @@ const toggleBreakdown = async (type: string, id: string) => {
|
||||
start_date: props.startDate,
|
||||
end_date: props.endDate,
|
||||
model: id,
|
||||
model_source: props.source,
|
||||
})
|
||||
breakdownItems.value = res.users || []
|
||||
} catch {
|
||||
@@ -272,6 +317,7 @@ const toggleBreakdown = async (type: string, id: string) => {
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:metric': [value: DistributionMetric]
|
||||
'update:source': [value: ModelSource]
|
||||
'ranking-click': [item: UserSpendingRankingItem]
|
||||
}>()
|
||||
|
||||
@@ -294,14 +340,19 @@ const chartColors = [
|
||||
]
|
||||
|
||||
const displayModelStats = computed(() => {
|
||||
if (!props.modelStats?.length) return []
|
||||
const sourceStats = props.source === 'upstream'
|
||||
? props.upstreamModelStats
|
||||
: props.source === 'mapping'
|
||||
? props.mappingModelStats
|
||||
: props.modelStats
|
||||
if (!sourceStats?.length) return []
|
||||
|
||||
const metricKey = props.metric === 'actual_cost' ? 'actual_cost' : 'total_tokens'
|
||||
return [...props.modelStats].sort((a, b) => b[metricKey] - a[metricKey])
|
||||
return [...sourceStats].sort((a, b) => b[metricKey] - a[metricKey])
|
||||
})
|
||||
|
||||
const chartData = computed(() => {
|
||||
if (!props.modelStats?.length) return null
|
||||
if (!displayModelStats.value.length) return null
|
||||
|
||||
return {
|
||||
labels: displayModelStats.value.map((m) => m.model),
|
||||
|
||||
Reference in New Issue
Block a user