feat(usage): show reasoning and fast badges in usage records

- extract provider reasoning effort from request body metadata
- extract priority service tier and expose it as service_tier
- show reasoning level and fast badges after model names
- include badges in active request updates and usage list payloads
- add targeted backend and frontend coverage
This commit is contained in:
AAEE86
2026-05-27 00:36:52 +08:00
parent e21cd72181
commit e27ca671fd
16 changed files with 371 additions and 13 deletions
@@ -187,7 +187,25 @@
<!-- 第一行:模型 + 费用 -->
<div class="flex items-center justify-between gap-2">
<div class="min-w-0 flex-1">
<span class="text-sm font-medium truncate block">{{ record.model }}</span>
<div class="flex min-w-0 items-center gap-1">
<span class="text-sm font-medium truncate">{{ record.model }}</span>
<Badge
v-if="getReasoningEffort(record)"
variant="outline"
class="h-4 rounded-full border-primary/30 bg-primary/5 px-1.5 text-[10px] leading-4 text-primary flex-shrink-0"
:title="getReasoningEffortTitle(record)"
>
{{ getReasoningEffort(record) }}
</Badge>
<Badge
v-if="getFastBadge(record)"
variant="outline"
class="h-4 rounded-full border-emerald-500/30 bg-emerald-500/10 px-1.5 text-[10px] leading-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0"
:title="getFastBadgeTitle(record)"
>
fast
</Badge>
</div>
<span
v-if="getActualModel(record)"
class="text-[11px] text-muted-foreground truncate block"
@@ -556,8 +574,24 @@
v-if="getActualModel(record)"
class="flex flex-col text-xs gap-0.5"
>
<div class="flex items-center gap-1 truncate">
<div class="flex min-w-0 items-center gap-1">
<span class="truncate">{{ record.model }}</span>
<Badge
v-if="getReasoningEffort(record)"
variant="outline"
class="h-4 rounded-full border-primary/30 bg-primary/5 px-1.5 text-[10px] leading-4 text-primary flex-shrink-0"
:title="getReasoningEffortTitle(record)"
>
{{ getReasoningEffort(record) }}
</Badge>
<Badge
v-if="getFastBadge(record)"
variant="outline"
class="h-4 rounded-full border-emerald-500/30 bg-emerald-500/10 px-1.5 text-[10px] leading-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0"
:title="getFastBadgeTitle(record)"
>
fast
</Badge>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
@@ -575,8 +609,26 @@
</div>
<span
v-else
class="truncate block"
>{{ record.model }}</span>
class="flex min-w-0 items-center gap-1"
>
<span class="truncate">{{ record.model }}</span>
<Badge
v-if="getReasoningEffort(record)"
variant="outline"
class="h-4 rounded-full border-primary/30 bg-primary/5 px-1.5 text-[10px] leading-4 text-primary flex-shrink-0"
:title="getReasoningEffortTitle(record)"
>
{{ getReasoningEffort(record) }}
</Badge>
<Badge
v-if="getFastBadge(record)"
variant="outline"
class="h-4 rounded-full border-emerald-500/30 bg-emerald-500/10 px-1.5 text-[10px] leading-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0"
:title="getFastBadgeTitle(record)"
>
fast
</Badge>
</span>
</TableCell>
<TableCell
v-if="isAdmin && isColumnVisible('provider')"
@@ -1302,12 +1354,39 @@ function getActualModel(record: UsageRecord): string | null {
return null
}
function getReasoningEffort(record: UsageRecord): string | null {
const effort = record.reasoning_effort?.trim()
return effort || null
}
function getReasoningEffortTitle(record: UsageRecord): string {
const effort = getReasoningEffort(record)
return effort ? `Reasoning: ${effort}` : ''
}
function getServiceTier(record: UsageRecord): string | null {
const serviceTier = record.service_tier?.trim().toLowerCase()
return serviceTier || null
}
function getFastBadge(record: UsageRecord): boolean {
return getServiceTier(record) === 'priority'
}
function getFastBadgeTitle(record: UsageRecord): string {
const serviceTier = getServiceTier(record)
return serviceTier ? `Service tier: ${serviceTier}` : ''
}
// 获取模型列的 tooltip
function getModelTooltip(record: UsageRecord): string {
const actualModel = getActualModel(record)
const reasoningEffort = getReasoningEffort(record)
const fastSuffix = getFastBadge(record) ? '\nService tier: priority' : ''
const suffix = `${reasoningEffort ? `\nReasoning: ${reasoningEffort}` : ''}${fastSuffix}`
if (actualModel) {
return `${record.model} -> ${actualModel}`
return `${record.model} -> ${actualModel}${suffix}`
}
return record.model
return `${record.model}${suffix}`
}
</script>
@@ -278,6 +278,20 @@ describe('UsageRecordsTable', () => {
expect(root.textContent).toContain('gpt-5')
})
it('shows reasoning effort next to the model name', () => {
const root = mountUsageRecordsTable([buildRecord({ reasoning_effort: 'xhigh' })])
expect(root.textContent).toContain('gpt-5')
expect(root.textContent).toContain('xhigh')
})
it('shows fast badge for priority service tier', () => {
const root = mountUsageRecordsTable([buildRecord({ service_tier: 'priority' })])
expect(root.textContent).toContain('gpt-5')
expect(root.textContent).toContain('fast')
})
it('offers embedding API formats in the usage record filter', () => {
const root = mountUsageRecordsTable([buildRecord({ api_format: 'openai:chat' })])
@@ -523,7 +523,9 @@ export function useUsageData(options: UseUsageDataOptions) {
api_key_name: existing.api_key_name || record.api_key_name,
provider_key_name: existing.provider_key_name || record.provider_key_name,
rate_multiplier: existing.rate_multiplier ?? record.rate_multiplier,
target_model: existing.target_model || record.target_model
target_model: existing.target_model || record.target_model,
reasoning_effort: existing.reasoning_effort || record.reasoning_effort,
service_tier: existing.service_tier || record.service_tier
}
}
+2
View File
@@ -96,6 +96,8 @@ export interface UsageRecord {
model: string
target_model?: string | null // 映射后的目标模型名(若无映射则为空)
model_version?: string | null // Provider 返回的实际模型版本(列表轻量字段)
reasoning_effort?: string | null // 从发送给 Provider 的请求体提取的 reasoning 级别
service_tier?: string | null // 从发送给 Provider 的请求体提取的服务层级
api_format?: string
endpoint_api_format?: string // 端点原生格式
has_format_conversion?: boolean // 是否发生了格式转换