fix(usage): 修复缓存创建 token 展示并在总量中计入缓存组件

- gateway/admin 用量载荷在 cache_creation_input_tokens 为 0 时回填 ephemeral_5m/1h 合计
- 标准化用量写入时将 cache_creation/cache_read token 计入 total_tokens
- 前端列表与轮询同步新增分类字段,修复缓存 token 列显示
This commit is contained in:
fawney19
2026-04-19 02:46:28 +08:00
parent e82c9c5104
commit 2f487fda66
11 changed files with 316 additions and 19 deletions

View File

@@ -61,6 +61,8 @@ export interface UsageRecordDetail {
is_stream: boolean
created_at: string
cache_creation_input_tokens?: number
cache_creation_ephemeral_5m_input_tokens?: number
cache_creation_ephemeral_1h_input_tokens?: number
cache_read_input_tokens?: number
status_code: number
error_message?: string
@@ -284,8 +286,11 @@ export const meApi = {
id: string
status: 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled'
input_tokens: number
effective_input_tokens?: number | null
output_tokens: number
cache_creation_input_tokens?: number | null
cache_creation_ephemeral_5m_input_tokens?: number | null
cache_creation_ephemeral_1h_input_tokens?: number | null
cache_read_input_tokens?: number | null
cost: number
actual_cost?: number | null
@@ -296,6 +301,7 @@ export const meApi = {
endpoint_api_format?: string | null
has_format_conversion?: boolean | null
has_fallback?: boolean | null
target_model?: string | null
}>
}> {
const params = ids ? { ids } : {}

View File

@@ -13,6 +13,8 @@ export interface UsageRecord {
effective_input_tokens?: number
output_tokens: number
cache_creation_input_tokens?: number
cache_creation_ephemeral_5m_input_tokens?: number
cache_creation_ephemeral_1h_input_tokens?: number
cache_read_input_tokens?: number
total_tokens: number
cost?: number
@@ -295,6 +297,8 @@ export const usageApi = {
effective_input_tokens?: number | null
output_tokens: number
cache_creation_input_tokens?: number | null
cache_creation_ephemeral_5m_input_tokens?: number | null
cache_creation_ephemeral_1h_input_tokens?: number | null
cache_read_input_tokens?: number | null
cost: number
actual_cost?: number | null

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest'
import { getCacheCreationTokens, getEffectiveInputTokens } from '../token-normalization'
describe('usage token normalization', () => {
it('prefers explicit cache creation totals when present', () => {
expect(getCacheCreationTokens({
cache_creation_input_tokens: 18,
cache_creation_ephemeral_5m_input_tokens: 5,
cache_creation_ephemeral_1h_input_tokens: 7,
})).toBe(18)
})
it('rehydrates cache creation totals from classified fields when legacy total is zero', () => {
expect(getCacheCreationTokens({
cache_creation_input_tokens: 0,
cache_creation_ephemeral_5m_input_tokens: 12,
cache_creation_ephemeral_1h_input_tokens: 8,
})).toBe(20)
})
it('keeps effective input token normalization unchanged', () => {
expect(getEffectiveInputTokens({
input_tokens: 100,
cache_read_input_tokens: 20,
api_format: 'openai:chat',
})).toBe(80)
})
})

View File

@@ -557,9 +557,9 @@
<span>{{ formatTokens(record.output_tokens || 0) }}</span>
</div>
<div class="flex items-center gap-1 text-muted-foreground">
<span :class="record.cache_creation_input_tokens ? 'text-foreground/70' : ''">{{ record.cache_creation_input_tokens ? formatTokens(record.cache_creation_input_tokens) : '-' }}</span>
<span :class="hasPositiveTokens(getRecordCacheCreationTokens(record)) ? 'text-foreground/70' : ''">{{ formatOptionalTokens(getRecordCacheCreationTokens(record)) }}</span>
<span>/</span>
<span :class="record.cache_read_input_tokens ? 'text-foreground/70' : ''">{{ record.cache_read_input_tokens ? formatTokens(record.cache_read_input_tokens) : '-' }}</span>
<span :class="hasPositiveTokens(record.cache_read_input_tokens) ? 'text-foreground/70' : ''">{{ formatOptionalTokens(record.cache_read_input_tokens) }}</span>
</div>
</div>
</TableCell>
@@ -677,7 +677,7 @@ import {
import { RefreshCcw, Search } from 'lucide-vue-next'
import { formatTokens, formatCurrency } from '@/utils/format'
import { formatDateTime } from '../composables'
import { getEffectiveInputTokens } from '../token-normalization'
import { getCacheCreationTokens, getEffectiveInputTokens } from '../token-normalization'
import { isUsageRecordFailed, resolveDisplayRequestStatus } from '../utils/status'
import { useRowClick } from '@/composables/useRowClick'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
@@ -787,6 +787,18 @@ function getRecordEffectiveInputTokens(record: UsageRecord): number {
return getEffectiveInputTokens(record)
}
function getRecordCacheCreationTokens(record: UsageRecord): number {
return getCacheCreationTokens(record)
}
function hasPositiveTokens(value: number | null | undefined): boolean {
return typeof value === 'number' && Number.isFinite(value) && value > 0
}
function formatOptionalTokens(value: number | null | undefined): string {
return hasPositiveTokens(value) ? formatTokens(value) : '-'
}
// useDebounceFn 自动处理清理,无需 onUnmounted
// 判断是否应该显示格式转换信息

View File

@@ -393,6 +393,12 @@ export function useUsageData(options: UseUsageDataOptions) {
effective_input_tokens: existing.effective_input_tokens ?? record.effective_input_tokens,
output_tokens: existing.output_tokens || record.output_tokens,
cache_creation_input_tokens: existing.cache_creation_input_tokens ?? record.cache_creation_input_tokens,
cache_creation_ephemeral_5m_input_tokens:
existing.cache_creation_ephemeral_5m_input_tokens
?? record.cache_creation_ephemeral_5m_input_tokens,
cache_creation_ephemeral_1h_input_tokens:
existing.cache_creation_ephemeral_1h_input_tokens
?? record.cache_creation_ephemeral_1h_input_tokens,
cache_read_input_tokens: existing.cache_read_input_tokens ?? record.cache_read_input_tokens,
cost: existing.cost || record.cost,
actual_cost: existing.actual_cost ?? record.actual_cost,

View File

@@ -1,6 +1,9 @@
type UsageTokenLike = {
effective_input_tokens?: number | null
input_tokens?: number | null
cache_creation_input_tokens?: number | null
cache_creation_ephemeral_5m_input_tokens?: number | null
cache_creation_ephemeral_1h_input_tokens?: number | null
cache_read_input_tokens?: number | null
api_format?: string | null
endpoint_api_format?: string | null
@@ -17,6 +20,16 @@ function apiFamily(apiFormat: string | null | undefined): string {
.toLowerCase()
}
export function getCacheCreationTokens(usage: UsageTokenLike): number {
const explicit = toNonNegativeNumber(usage.cache_creation_input_tokens)
const classified = toNonNegativeNumber(usage.cache_creation_ephemeral_5m_input_tokens)
+ toNonNegativeNumber(usage.cache_creation_ephemeral_1h_input_tokens)
if (explicit === 0 && classified > 0) {
return classified
}
return explicit
}
export function getEffectiveInputTokens(usage: UsageTokenLike): number {
const explicit = toNonNegativeNumber(usage.effective_input_tokens)
if (explicit > 0) {

View File

@@ -98,6 +98,8 @@ export interface UsageRecord {
effective_input_tokens?: number
output_tokens: number
cache_creation_input_tokens?: number
cache_creation_ephemeral_5m_input_tokens?: number
cache_creation_ephemeral_1h_input_tokens?: number
cache_read_input_tokens?: number
total_tokens: number
cost: number

View File

@@ -391,6 +391,10 @@ async function pollActiveRequests() {
record.effective_input_tokens = update.effective_input_tokens ?? record.effective_input_tokens
record.output_tokens = update.output_tokens
record.cache_creation_input_tokens = update.cache_creation_input_tokens ?? undefined
record.cache_creation_ephemeral_5m_input_tokens =
update.cache_creation_ephemeral_5m_input_tokens ?? undefined
record.cache_creation_ephemeral_1h_input_tokens =
update.cache_creation_ephemeral_1h_input_tokens ?? undefined
record.cache_read_input_tokens = update.cache_read_input_tokens ?? undefined
record.cost = update.cost
record.actual_cost = update.actual_cost ?? undefined