merge(main): 解决 usage 模型展示契约冲突

This commit is contained in:
MMEXA
2026-07-18 19:26:14 +08:00
12 changed files with 446 additions and 61 deletions
@@ -9,7 +9,7 @@
>
<div
class="flex min-w-0 max-w-full items-center gap-1"
:class="modelRowClass"
:class="[modelRowClass, actualModel ? 'flex-wrap' : '']"
>
<span
class="min-w-0 truncate"
@@ -17,12 +17,10 @@
data-usage-model-source
>{{ record.model }}</span>
<template v-if="actualModel">
<span class="shrink-0 text-muted-foreground/70">-&gt;</span>
<span
class="min-w-0 truncate"
:class="modelClass"
class="order-last basis-full min-w-0 break-all whitespace-normal text-muted-foreground"
data-usage-model-target
>{{ actualModel }}</span>
><span class="mr-1">-&gt;</span>{{ actualModel }}</span>
</template>
<template v-if="!shouldStackBadges">
<Badge
@@ -71,7 +69,7 @@ import { Badge } from '@/components/ui'
import { isCyberPolicyError } from '../utils/cyberError'
import { formatServiceTierFact } from '../utils/service-tier'
type ModelBadgeKey = 'compact' | 'reasoning' | 'fast' | 'cyber'
type ModelBadgeKey = 'compact' | 'reasoning' | 'fast' | 'cyber' | 'reasoning_tokens'
interface ModelBadgePresentation {
key: ModelBadgeKey
@@ -90,6 +88,7 @@ interface UsageModelDisplayRecord {
requested_reasoning_effort?: string | null
reasoning_effort?: string | null
service_tier?: string | null
reasoning_tokens?: number
error_message?: string | null
}
@@ -100,12 +99,18 @@ const props = withDefaults(defineProps<{
context?: 'usage' | 'detail'
cyber?: boolean | null
stackFullWidth?: boolean
showServiceTierBadge?: boolean
showCyberBadge?: boolean
showReasoningBadge?: boolean
}>(), {
modelClass: '',
modelRowClass: '',
context: 'usage',
cyber: null,
stackFullWidth: false,
showServiceTierBadge: true,
showCyberBadge: true,
showReasoningBadge: true,
})
const actualModel = computed(() => {
@@ -138,7 +143,7 @@ const modelBadges = computed<ModelBadgePresentation[]>(() => {
ariaLabel: '会话压缩',
})
}
if (reasoningLabel.value) {
if (props.showReasoningBadge && reasoningLabel.value) {
badges.push({
key: 'reasoning',
label: reasoningLabel.value,
@@ -149,36 +154,51 @@ const modelBadges = computed<ModelBadgePresentation[]>(() => {
})
}
if (formatServiceTierFact(props.record.service_tier) === 'Fast') {
if (props.showServiceTierBadge && formatServiceTierFact(props.record.service_tier) === 'Fast') {
badges.push({
key: 'fast',
label: 'Fast',
variant: 'outline-transparent',
className: 'text-amber-700 dark:text-amber-300',
className: 'text-blue-500 dark:text-blue-300',
title: '上游请求档位:Fast\n计费档位:Fast',
ariaLabel: '上游请求档位:Fast,计费档位:Fast',
})
}
if (props.cyber ?? isCyberPolicyError(props.record.error_message)) {
if (props.showCyberBadge && (props.cyber ?? isCyberPolicyError(props.record.error_message))) {
badges.push({
key: 'cyber',
label: 'Cyber',
variant: 'outline',
className: 'border-primary/30 bg-primary/5 text-rose-600 dark:text-rose-300',
className: 'border-primary/30 bg-primary/5 text-rose-500 dark:text-rose-300',
title: '上游 Cyber Policy 拒绝',
ariaLabel: '上游 Cyber Policy 拒绝',
})
}
if (typeof props.record.reasoning_tokens === 'number' && props.record.reasoning_tokens > 0) {
badges.push({
key: 'reasoning_tokens',
label: `推理 ${formatCompactTokens(props.record.reasoning_tokens)}`,
variant: 'outline-transparent',
className: 'text-muted-foreground',
title: `推理 Token 数:${props.record.reasoning_tokens}`,
ariaLabel: `推理 Token 数:${props.record.reasoning_tokens}`,
})
}
return badges
})
const shouldStackBadges = computed(() => (
actualModel.value !== null || modelBadges.value.length >= 3
actualModel.value === null && modelBadges.value.length >= 3
))
function normalizeText(value: string | null | undefined): string | null {
const normalized = value?.trim()
return normalized || null
}
function formatCompactTokens(value: number): string {
if (value < 1000) return `${value} Tokens`
return `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)}K Tokens`
}
</script>
@@ -1191,7 +1191,10 @@ function sanitizeColumnIds(
seen.add(id as UsageRecordColumnId)
return true
})
return sanitized.length > 0 ? sanitized : [...fallback]
if (sanitized.length === 0) return [...fallback]
// Add newly introduced feature column to existing saved layouts, keeping it
// immediately before Tokens as the default presentation order.
return sanitized
}
const visibleColumnIds = computed<UsageRecordColumnId[]>({
@@ -1579,7 +1582,7 @@ function buildServiceTierBadgePresentation(
const title = titleLines.join('\n')
return {
label: 'Fast',
className: '!bg-transparent text-amber-700 dark:text-amber-300',
className: '!bg-transparent text-blue-500 dark:text-blue-300',
title,
ariaLabel: titleLines.join(','),
}
@@ -165,19 +165,21 @@ describe('RequestDetailDrawer settlement pricing', () => {
expect(document.body.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
.toContain('Cyber')
const modelLayout = document.body.querySelector(
'[data-request-detail-model-layout="stacked"]',
'[data-request-detail-model-layout="inline"]',
)
expect(modelLayout?.firstElementChild?.textContent).toContain('gpt-5')
expect(modelLayout?.firstElementChild?.textContent).toContain('->')
expect(modelLayout?.firstElementChild?.textContent).toContain('gpt-5.1')
expect(modelLayout?.firstElementChild?.querySelector('[data-request-detail-model-badge]'))
.toBeNull()
const modelBadgesRow = modelLayout?.querySelector(
'[data-request-detail-model-badges-row]',
)
expect(modelBadgesRow?.textContent).toContain('xhigh -> max')
expect(modelBadgesRow?.textContent).toContain('Fast')
expect(modelBadgesRow?.textContent).toContain('Cyber')
const modelRow = modelLayout?.firstElementChild
expect(modelRow?.textContent).toContain('gpt-5')
expect(modelRow?.textContent).toContain('->')
expect(modelRow?.textContent).toContain('gpt-5.1')
expect(modelRow?.querySelector('[data-usage-model-target]')?.classList.contains('basis-full'))
.toBe(true)
expect(modelRow?.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
.toContain('xhigh -> max')
expect(modelRow?.querySelector('[data-request-detail-model-badge="fast"]')?.textContent)
.toContain('Fast')
expect(modelRow?.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
.toContain('Cyber')
expect(modelLayout?.querySelector('[data-request-detail-model-badges-row]')).toBeNull()
const serviceTierFacts = document.body.querySelector('[data-testid="service-tier-facts"]')
expect([...serviceTierFacts?.querySelectorAll('dt') ?? []].map(node => node.textContent?.trim()))
.toEqual(['上游请求层级', '计费层级'])
@@ -359,7 +361,7 @@ describe('RequestDetailDrawer settlement pricing', () => {
await vi.waitFor(() => {
expect(apiMocks.getRequestDetail).toHaveBeenCalledTimes(1)
expect(document.body.querySelector('[data-usage-model-target]')?.textContent?.trim())
.toBe('gpt-5.1')
.toBe('->gpt-5.1')
expect(document.body.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent?.trim())
.toBe('xhigh -> max')
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')).toBeNull()
@@ -414,7 +416,7 @@ describe('RequestDetailDrawer settlement pricing', () => {
await vi.waitFor(() => {
expect(document.body.querySelector('[data-usage-model-target]')?.textContent?.trim())
.toBe('gpt-5.1-2026-07-17')
.toBe('->gpt-5.1-2026-07-17')
})
})
@@ -327,7 +327,7 @@ describe('UsageRecordsTable', () => {
.toBe('会话压缩')
})
it('shows mapping, reasoning, Fast, and Cyber together in the model area', () => {
it('shows mapping, reasoning, Fast, and Cyber in the model area', () => {
const root = mountUsageRecordsTable([buildRecord({
model: 'gpt-5',
target_model: 'gpt-5.1',
@@ -364,25 +364,25 @@ describe('UsageRecordsTable', () => {
expect(fastBadge?.classList.contains('border-amber-400/50')).toBe(false)
expect(fastBadge?.classList.contains('!bg-transparent')).toBe(false)
expect(fastBadge?.classList.contains('bg-amber-400/10')).toBe(false)
expect(fastBadge?.classList.contains('text-amber-700')).toBe(true)
expect(fastBadge?.classList.contains('text-blue-500')).toBe(true)
expect(cyberBadge?.classList.contains('border-primary/30')).toBe(true)
expect(cyberBadge?.classList.contains('bg-primary/5')).toBe(true)
expect(cyberBadge?.classList.contains('text-rose-600')).toBe(true)
expect(cyberBadge?.classList.contains('text-rose-500')).toBe(true)
expect(cyberBadges.length).toBeGreaterThan(0)
expect([...cyberBadges].every(badge => badge.textContent?.trim() === 'Cyber')).toBe(true)
expect([...cyberBadges].every(badge => badge.title === '上游 Cyber Policy 拒绝')).toBe(true)
const stackedLayout = root.querySelector('[data-usage-model-layout="stacked"]')
expect(stackedLayout).not.toBeNull()
const modelRow = stackedLayout?.firstElementChild
const inlineLayout = root.querySelector('[data-usage-model-layout="inline"]')
expect(inlineLayout).not.toBeNull()
const modelRow = inlineLayout?.firstElementChild
expect(modelRow?.textContent).toContain('gpt-5')
expect(modelRow?.textContent).toContain('->')
expect(modelRow?.textContent).toContain('gpt-5.1')
expect(modelRow?.querySelector('[data-usage-model-badge]')).toBeNull()
const badgesRow = stackedLayout?.querySelector('[data-usage-model-badges-row]')
expect(badgesRow?.textContent).toContain('xhigh -> max')
expect(badgesRow?.textContent).toContain('Fast')
expect(badgesRow?.textContent).toContain('Cyber')
expect(modelRow?.querySelector('[data-usage-model-target]')?.classList.contains('basis-full')).toBe(true)
expect(modelRow?.querySelector('[data-usage-model-target]')?.classList.contains('order-last')).toBe(true)
expect(modelRow?.querySelector('[data-usage-model-badge="reasoning"]')?.textContent).toContain('xhigh -> max')
expect(modelRow?.querySelector('[data-usage-model-badge="fast"]')?.textContent).toContain('Fast')
expect(modelRow?.querySelector('[data-usage-model-badge="cyber"]')?.textContent).toContain('Cyber')
})
it('stacks three model badges even without a model mapping', () => {
@@ -398,13 +398,10 @@ describe('UsageRecordsTable', () => {
})])
const stackedLayout = root.querySelector('[data-usage-model-layout="stacked"]')
expect(stackedLayout?.firstElementChild?.textContent?.trim()).toBe('gpt-5')
expect(stackedLayout?.querySelector('[data-usage-model-badges-row]')?.textContent)
.toContain('xhigh')
expect(stackedLayout?.querySelector('[data-usage-model-badges-row]')?.textContent)
.toContain('Fast')
expect(stackedLayout?.querySelector('[data-usage-model-badges-row]')?.textContent)
.toContain('Cyber')
expect(stackedLayout?.textContent).toContain('gpt-5')
expect(stackedLayout?.textContent).toContain('xhigh')
expect(stackedLayout?.textContent).toContain('Fast')
expect(stackedLayout?.textContent).toContain('Cyber')
})
it.each(['priority', 'fast', ' Priority ', 'FAST'])(
@@ -605,6 +605,7 @@ export function useUsageData(options: UseUsageDataOptions) {
input_tokens: mergeSparseRecordMetric(existing.input_tokens, record.input_tokens) ?? record.input_tokens,
effective_input_tokens: mergeSparseRecordMetric(existing.effective_input_tokens, record.effective_input_tokens) ?? record.effective_input_tokens,
output_tokens: mergeSparseRecordMetric(existing.output_tokens, record.output_tokens) ?? record.output_tokens,
reasoning_tokens: mergeSparseRecordMetric(existing.reasoning_tokens, record.reasoning_tokens) ?? record.reasoning_tokens,
total_tokens: mergeSparseRecordMetric(existing.total_tokens, record.total_tokens) ?? record.total_tokens,
cache_creation_input_tokens: mergeSparseRecordMetric(existing.cache_creation_input_tokens, record.cache_creation_input_tokens) ?? record.cache_creation_input_tokens,
cache_creation_ephemeral_5m_input_tokens:
+1
View File
@@ -107,6 +107,7 @@ export interface UsageRecord {
input_tokens: number
effective_input_tokens?: number
output_tokens: number
reasoning_tokens?: number
cache_creation_input_tokens?: number
cache_creation_ephemeral_5m_input_tokens?: number
cache_creation_ephemeral_1h_input_tokens?: number