mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 01:47:47 +08:00
fix(usage): align fast-tier pricing and model metadata
This commit is contained in:
@@ -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">-></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">-></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 = 'reasoning' | 'fast' | 'cyber'
|
||||
type ModelBadgeKey = 'reasoning' | 'fast' | 'cyber' | 'reasoning_tokens'
|
||||
|
||||
interface ModelBadgePresentation {
|
||||
key: ModelBadgeKey
|
||||
@@ -89,6 +87,7 @@ interface UsageModelDisplayRecord {
|
||||
requested_reasoning_effort?: string | null
|
||||
reasoning_effort?: string | null
|
||||
service_tier?: string | null
|
||||
reasoning_tokens?: number
|
||||
error_message?: string | null
|
||||
}
|
||||
|
||||
@@ -99,12 +98,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(() => {
|
||||
@@ -127,7 +132,7 @@ const reasoningLabel = computed(() => {
|
||||
|
||||
const modelBadges = computed<ModelBadgePresentation[]>(() => {
|
||||
const badges: ModelBadgePresentation[] = []
|
||||
if (reasoningLabel.value) {
|
||||
if (props.showReasoningBadge && reasoningLabel.value) {
|
||||
badges.push({
|
||||
key: 'reasoning',
|
||||
label: reasoningLabel.value,
|
||||
@@ -138,36 +143,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(','),
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ describe('UsageRecordsTable', () => {
|
||||
.toBe('Fast')
|
||||
})
|
||||
|
||||
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',
|
||||
@@ -343,25 +343,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', () => {
|
||||
@@ -377,13 +377,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:
|
||||
|
||||
@@ -106,6 +106,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
|
||||
|
||||
Reference in New Issue
Block a user