feat: 扩展 cache creation token 细分统计与 effective_input_tokens 计费逻辑

- 新增 cache_creation_ephemeral_5m/1h_input_tokens 字段,区分不同 TTL 的缓存写入 token
- 引入 effective_input_tokens(扣除 cache read 后的有效输入 token),暴露给 usage 接口
- billing 规则生成器支持 5m/1h ephemeral cache 独立定价与分级计费
- usage_mapper 增加 Claude/Anthropic 格式映射,修复 OpenAI responses 格式字段兼容性
- 迁移逻辑增强:支持 checksum 容错、applied/pending 数量日志、逐步执行信息输出
- executor 抽离 LocalExecutionRequestOutcome 类型,统一 sync/stream 路径返回语义
- provider-transport auth 层新增 complete passthrough headers 构建逻辑
- 前端 usage 类型全面补充 effective_input_tokens、cache_creation_tokens、total_input_context 字段
This commit is contained in:
fawney19
2026-04-10 17:44:55 +08:00
parent 5014e2f5fd
commit 010ab127e2
64 changed files with 4217 additions and 477 deletions
@@ -194,7 +194,7 @@
<!-- 阶梯标题 -->
<div class="text-xs text-muted-foreground flex items-center gap-2 flex-wrap">
<span class="font-medium text-foreground">Token 计费</span>
<span class="text-muted-foreground/60">(输入 {{ formatNumber(detail.tokens?.input || detail.input_tokens || 0) }} + 缓存创建 {{ cacheCreationSummaryText }} + 缓存读取 {{ formatNumber(detail.cache_read_input_tokens || 0) }})</span>
<span class="text-muted-foreground/60">(输入 {{ formatNumber(displayInputTokens) }} + 缓存创建 {{ cacheCreationSummaryText }} + 缓存读取 {{ formatNumber(detail.cache_read_input_tokens || 0) }})</span>
<Badge
v-if="displayTiers.length > 1"
variant="outline"
@@ -260,7 +260,7 @@
<div class="flex items-center">
<div class="flex items-center flex-1">
<span class="text-xs text-muted-foreground w-[56px]">输入</span>
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.tokens?.input || detail.input_tokens || 0 }}</span>
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ displayInputTokens }}</span>
<span class="text-xs font-mono">${{ (detail.cost?.input || detail.input_cost || 0).toFixed(6) }}</span>
</div>
<Separator
@@ -701,6 +701,7 @@ import { dashboardApi, type RequestDetail } from '@/api/dashboard'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
import { formatShortRequestId } from '@/utils/format'
import { log } from '@/utils/logger'
import { getEffectiveInputTokens } from '../token-normalization'
// 子组件
import RequestHeadersContent from './RequestDetailDrawer/RequestHeadersContent.vue'
@@ -774,6 +775,16 @@ let timelineMountTimer: ReturnType<typeof setTimeout> | null = null
const fullRequestId = computed(() => detail.value?.request_id || detail.value?.id || '-')
const displayRequestId = computed(() => formatShortRequestId(fullRequestId.value))
const displayInputTokens = computed(() => {
if (!detail.value) return 0
return getEffectiveInputTokens({
effective_input_tokens: detail.value.effective_input_tokens,
input_tokens: detail.value.input_tokens ?? detail.value.tokens?.input,
cache_read_input_tokens: detail.value.cache_read_input_tokens,
api_format: detail.value.api_format,
endpoint_api_format: detail.value.endpoint_api_format,
})
})
// 监听标签页切换
watch(activeTab, (newTab) => {
@@ -53,7 +53,7 @@
</TableCell>
<TableCell class="text-right py-2 px-2">
<div class="flex flex-col items-end text-xs gap-0.5 whitespace-nowrap">
<span>{{ formatTokens(item.total_input_context || 0) }} / {{ formatTokens(item.output_tokens || 0) }}</span>
<span>{{ formatTokens(item.effective_input_tokens ?? item.total_input_context ?? 0) }} / {{ formatTokens(item.output_tokens || 0) }}</span>
<span class="text-muted-foreground">{{ formatTokens((item.cache_read_tokens || 0) + (item.cache_creation_tokens || 0)) }}</span>
</div>
</TableCell>
@@ -53,7 +53,7 @@
</TableCell>
<TableCell class="text-right py-2 px-2">
<div class="flex flex-col items-end text-xs gap-0.5 whitespace-nowrap">
<span>{{ formatTokens(model.total_input_context || 0) }} / {{ formatTokens(model.output_tokens || 0) }}</span>
<span>{{ formatTokens(model.effective_input_tokens ?? model.total_input_context ?? 0) }} / {{ formatTokens(model.output_tokens || 0) }}</span>
<span class="text-muted-foreground">{{ formatTokens(model.cache_read_tokens || 0) }}</span>
</div>
</TableCell>
@@ -56,7 +56,7 @@
</TableCell>
<TableCell class="text-right py-2 px-2">
<div class="flex flex-col items-end text-xs gap-0.5 whitespace-nowrap">
<span>{{ formatTokens(provider.totalInputContext || 0) }} / {{ formatTokens(provider.outputTokens || 0) }}</span>
<span>{{ formatTokens(provider.effectiveInputTokens ?? provider.totalInputContext ?? 0) }} / {{ formatTokens(provider.outputTokens || 0) }}</span>
<span class="text-muted-foreground">{{ formatTokens((provider.cacheReadTokens || 0) + (provider.cacheCreationTokens || 0)) }}</span>
</div>
</TableCell>
@@ -273,7 +273,7 @@
>-</span>
<span class="text-muted-foreground/50">|</span>
<!-- Tokens -->
<span>{{ formatTokens(record.input_tokens || 0) }}/{{ formatTokens(record.output_tokens || 0) }}</span>
<span>{{ formatTokens(getRecordEffectiveInputTokens(record)) }}/{{ formatTokens(record.output_tokens || 0) }}</span>
</div>
</div>
</div>
@@ -552,7 +552,7 @@
<TableCell class="text-right py-4 w-[140px]">
<div class="flex flex-col items-end text-xs gap-0.5">
<div class="flex items-center gap-1">
<span>{{ formatTokens(record.input_tokens || 0) }}</span>
<span>{{ formatTokens(getRecordEffectiveInputTokens(record)) }}</span>
<span class="text-muted-foreground">/</span>
<span>{{ formatTokens(record.output_tokens || 0) }}</span>
</div>
@@ -677,6 +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 { useRowClick } from '@/composables/useRowClick'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
import type { DateRangeParams, UsageRecord } from '../types'
@@ -777,6 +778,10 @@ function handleRowClick(event: MouseEvent, id: string) {
emit('showDetail', id)
}
function getRecordEffectiveInputTokens(record: UsageRecord): number {
return getEffectiveInputTokens(record)
}
// useDebounceFn 自动处理清理,无需 onUnmounted
// 判断是否应该显示格式转换信息