mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(usage): 修复缓存命中率计算并新增用户端 API 格式统计
- 新增 input_context_expr() 按 api_format 区分 input_tokens 语义 (OpenAI/Gemini input_tokens 已含 cache_read,Claude 需额外加上) - 缓存命中率统一改为基于归一化后的 total_input_context 计算 - 用户 /me/usage 接口新增 summary_by_api_format 后端聚合字段 - 前端 API 格式统计改用后端聚合数据,移除前端逐条记录手动统计 - 提取 formatHitRate 到 utils/format.ts 消除三处重复定义 - 移除 PoolManager 中未使用的 select_key 方法 Co-Authored-By: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
@@ -79,6 +79,8 @@ export interface ModelSummary {
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
total_tokens: number
|
||||
cache_read_tokens?: number
|
||||
cache_hit_rate?: number
|
||||
total_cost_usd: number
|
||||
actual_total_cost_usd?: number // 倍率消耗(仅管理员可见)
|
||||
}
|
||||
@@ -88,11 +90,24 @@ export interface ProviderSummary {
|
||||
provider: string
|
||||
requests: number
|
||||
total_tokens: number
|
||||
cache_read_tokens?: number
|
||||
cache_hit_rate?: number
|
||||
total_cost_usd: number
|
||||
success_rate: number | null
|
||||
avg_response_time_ms: number | null
|
||||
}
|
||||
|
||||
// API 格式统计接口
|
||||
export interface ApiFormatSummary {
|
||||
api_format: string
|
||||
request_count: number
|
||||
total_tokens: number
|
||||
cache_read_tokens: number
|
||||
cache_hit_rate: number
|
||||
total_cost_usd: number
|
||||
avg_response_time_ms: number
|
||||
}
|
||||
|
||||
// 使用统计响应接口
|
||||
export interface UsageResponse {
|
||||
total_requests: number
|
||||
@@ -105,6 +120,7 @@ export interface UsageResponse {
|
||||
billing: BillingSummary
|
||||
summary_by_model: ModelSummary[]
|
||||
summary_by_provider?: ProviderSummary[]
|
||||
summary_by_api_format?: ApiFormatSummary[]
|
||||
pagination?: {
|
||||
total: number
|
||||
limit: number
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface UsageByModel {
|
||||
total_tokens: number
|
||||
total_cost: number
|
||||
avg_response_time?: number
|
||||
cache_read_tokens?: number
|
||||
cache_hit_rate?: number
|
||||
}
|
||||
|
||||
export interface UsageByUser {
|
||||
@@ -61,6 +63,8 @@ export interface UsageByProvider {
|
||||
avg_response_time_ms: number
|
||||
success_rate: number
|
||||
error_count: number
|
||||
cache_read_tokens?: number
|
||||
cache_hit_rate?: number
|
||||
}
|
||||
|
||||
export interface UsageByApiFormat {
|
||||
@@ -70,6 +74,8 @@ export interface UsageByApiFormat {
|
||||
total_cost: number
|
||||
actual_cost: number
|
||||
avg_response_time_ms: number
|
||||
cache_read_tokens?: number
|
||||
cache_hit_rate?: number
|
||||
}
|
||||
|
||||
export interface UsageFilters {
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
费用
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
缓存Token
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
缓存命中率
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
平均响应
|
||||
</TableHead>
|
||||
@@ -29,7 +35,7 @@
|
||||
<TableBody>
|
||||
<TableRow v-if="data.length === 0">
|
||||
<TableCell
|
||||
:colspan="5"
|
||||
:colspan="7"
|
||||
class="text-center py-6 text-muted-foreground px-2"
|
||||
>
|
||||
暂无API格式统计数据
|
||||
@@ -59,6 +65,12 @@
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
{{ formatTokens(item.cache_read_tokens || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2 text-muted-foreground">
|
||||
{{ formatHitRate(item.cache_hit_rate) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground py-2 px-2">
|
||||
{{ item.avgResponseTime }}
|
||||
</TableCell>
|
||||
@@ -77,7 +89,7 @@ import TableBody from '@/components/ui/table-body.vue'
|
||||
import TableRow from '@/components/ui/table-row.vue'
|
||||
import TableHead from '@/components/ui/table-head.vue'
|
||||
import TableCell from '@/components/ui/table-cell.vue'
|
||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||
import { formatTokens, formatCurrency, formatHitRate } from '@/utils/format'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import type { ApiFormatStatsItem } from '../types'
|
||||
|
||||
@@ -85,5 +97,4 @@ defineProps<{
|
||||
data: ApiFormatStatsItem[]
|
||||
isAdmin: boolean
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
费用
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
缓存Token
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
缓存命中率
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
效率
|
||||
</TableHead>
|
||||
@@ -29,7 +35,7 @@
|
||||
<TableBody>
|
||||
<TableRow v-if="data.length === 0">
|
||||
<TableCell
|
||||
:colspan="5"
|
||||
:colspan="7"
|
||||
class="text-center py-6 text-muted-foreground px-2"
|
||||
>
|
||||
暂无模型统计数据
|
||||
@@ -59,6 +65,12 @@
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
{{ formatTokens(model.cache_read_tokens || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2 text-muted-foreground">
|
||||
{{ formatHitRate(model.cache_hit_rate) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-muted-foreground py-2 px-2">
|
||||
{{ model.costPerToken }}
|
||||
</TableCell>
|
||||
@@ -77,7 +89,7 @@ import TableBody from '@/components/ui/table-body.vue'
|
||||
import TableRow from '@/components/ui/table-row.vue'
|
||||
import TableHead from '@/components/ui/table-head.vue'
|
||||
import TableCell from '@/components/ui/table-cell.vue'
|
||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||
import { formatTokens, formatCurrency, formatHitRate } from '@/utils/format'
|
||||
import type { EnhancedModelStatsItem } from '../types'
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
费用
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
缓存Token
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
缓存命中率
|
||||
</TableHead>
|
||||
<TableHead class="h-8 px-2 text-right">
|
||||
成功率
|
||||
</TableHead>
|
||||
@@ -32,7 +38,7 @@
|
||||
<TableBody>
|
||||
<TableRow v-if="data.length === 0">
|
||||
<TableCell
|
||||
:colspan="6"
|
||||
:colspan="8"
|
||||
class="text-center py-6 text-muted-foreground px-2"
|
||||
>
|
||||
暂无提供商统计数据
|
||||
@@ -62,6 +68,12 @@
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
{{ formatTokens(provider.cacheReadTokens || 0) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2 text-muted-foreground">
|
||||
{{ formatHitRate(provider.cacheHitRate) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right py-2 px-2">
|
||||
<span :class="getSuccessRateClass(provider.successRate)">{{ provider.successRate }}%</span>
|
||||
</TableCell>
|
||||
@@ -83,7 +95,7 @@ import TableBody from '@/components/ui/table-body.vue'
|
||||
import TableRow from '@/components/ui/table-row.vue'
|
||||
import TableHead from '@/components/ui/table-head.vue'
|
||||
import TableCell from '@/components/ui/table-cell.vue'
|
||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||
import { formatTokens, formatCurrency, formatHitRate } from '@/utils/format'
|
||||
import type { ProviderStatsItem } from '../types'
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -110,6 +110,8 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
model: item.model,
|
||||
request_count: item.request_count || 0,
|
||||
total_tokens: item.total_tokens || 0,
|
||||
cache_read_tokens: typeof raw.cache_read_tokens === 'number' ? raw.cache_read_tokens : 0,
|
||||
cache_hit_rate: typeof raw.cache_hit_rate === 'number' ? raw.cache_hit_rate : 0,
|
||||
total_cost: item.total_cost || 0,
|
||||
actual_cost: typeof raw.actual_cost === 'number' ? raw.actual_cost : undefined
|
||||
}
|
||||
@@ -119,6 +121,8 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
provider: item.provider,
|
||||
requests: item.request_count,
|
||||
totalTokens: item.total_tokens || 0,
|
||||
cacheReadTokens: item.cache_read_tokens || 0,
|
||||
cacheHitRate: item.cache_hit_rate || 0,
|
||||
totalCost: item.total_cost,
|
||||
actualCost: item.actual_cost,
|
||||
successRate: item.success_rate,
|
||||
@@ -131,6 +135,8 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
api_format: item.api_format,
|
||||
request_count: item.request_count || 0,
|
||||
total_tokens: item.total_tokens || 0,
|
||||
cache_read_tokens: item.cache_read_tokens || 0,
|
||||
cache_hit_rate: item.cache_hit_rate || 0,
|
||||
total_cost: item.total_cost || 0,
|
||||
actual_cost: item.actual_cost,
|
||||
avgResponseTime: item.avg_response_time_ms > 0
|
||||
@@ -163,6 +169,8 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
model: item.model,
|
||||
request_count: item.requests || 0,
|
||||
total_tokens: item.total_tokens || 0,
|
||||
cache_read_tokens: item.cache_read_tokens || 0,
|
||||
cache_hit_rate: item.cache_hit_rate || 0,
|
||||
total_cost: item.total_cost_usd || 0,
|
||||
actual_cost: item.actual_total_cost_usd
|
||||
}))
|
||||
@@ -170,7 +178,9 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
providerStats.value = (userData.summary_by_provider || []).map((item) => ({
|
||||
provider: item.provider,
|
||||
requests: item.requests || 0,
|
||||
totalTokens: 0,
|
||||
totalTokens: item.total_tokens || 0,
|
||||
cacheReadTokens: item.cache_read_tokens || 0,
|
||||
cacheHitRate: item.cache_hit_rate || 0,
|
||||
totalCost: item.total_cost_usd || 0,
|
||||
successRate: item.success_rate || 0,
|
||||
avgResponseTime: (item.avg_response_time_ms ?? 0) > 0
|
||||
@@ -184,57 +194,28 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
currentRecords.value = mergeRecordStatus(currentRecords.value, nextRecords)
|
||||
totalRecords.value = userData.pagination?.total ?? currentRecords.value.length
|
||||
|
||||
// 从记录中提取筛选选项和 API 格式统计
|
||||
// 从记录中提取筛选选项
|
||||
const models = new Set<string>()
|
||||
const providers = new Set<string>()
|
||||
const apiFormatMap = new Map<string, {
|
||||
count: number
|
||||
tokens: number
|
||||
cost: number
|
||||
totalResponseTime: number
|
||||
responseTimeCount: number
|
||||
}>()
|
||||
|
||||
currentRecords.value.forEach(record => {
|
||||
if (record.model) models.add(record.model)
|
||||
if (record.provider) providers.add(record.provider)
|
||||
if (record.api_format) {
|
||||
const existing = apiFormatMap.get(record.api_format) || {
|
||||
count: 0,
|
||||
tokens: 0,
|
||||
cost: 0,
|
||||
totalResponseTime: 0,
|
||||
responseTimeCount: 0
|
||||
}
|
||||
existing.count++
|
||||
existing.tokens += record.total_tokens || 0
|
||||
existing.cost += record.cost || 0
|
||||
if (record.response_time_ms) {
|
||||
existing.totalResponseTime += record.response_time_ms
|
||||
existing.responseTimeCount++
|
||||
}
|
||||
apiFormatMap.set(record.api_format, existing)
|
||||
}
|
||||
})
|
||||
|
||||
availableModels.value = Array.from(models).sort()
|
||||
availableProviders.value = Array.from(providers).sort()
|
||||
|
||||
// 构建 API 格式统计数据
|
||||
apiFormatStats.value = Array.from(apiFormatMap.entries())
|
||||
.map(([format, data]) => {
|
||||
const avgMs = data.responseTimeCount > 0
|
||||
? data.totalResponseTime / data.responseTimeCount
|
||||
: 0
|
||||
return {
|
||||
api_format: format,
|
||||
request_count: data.count,
|
||||
total_tokens: data.tokens,
|
||||
total_cost: data.cost,
|
||||
avgResponseTime: avgMs > 0 ? `${(avgMs / 1000).toFixed(2)}s` : '-'
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.request_count - a.request_count)
|
||||
// API 格式统计直接使用后端聚合数据
|
||||
apiFormatStats.value = (userData.summary_by_api_format || []).map(item => ({
|
||||
api_format: item.api_format,
|
||||
request_count: item.request_count || 0,
|
||||
total_tokens: item.total_tokens || 0,
|
||||
cache_read_tokens: item.cache_read_tokens || 0,
|
||||
cache_hit_rate: item.cache_hit_rate || 0,
|
||||
total_cost: item.total_cost_usd || 0,
|
||||
avgResponseTime: (item.avg_response_time_ms ?? 0) > 0
|
||||
? `${((item.avg_response_time_ms ?? 0) / 1000).toFixed(2)}s`
|
||||
: '-'
|
||||
}))
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (requestId !== loadStatsRequestId) {
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface ModelStatsItem {
|
||||
model: string
|
||||
request_count: number
|
||||
total_tokens: number
|
||||
cache_read_tokens?: number
|
||||
cache_hit_rate?: number
|
||||
total_cost: number
|
||||
actual_cost?: number // 倍率消耗
|
||||
}
|
||||
@@ -36,6 +38,8 @@ export interface ProviderStatsItem {
|
||||
provider: string
|
||||
requests: number
|
||||
totalTokens: number
|
||||
cacheReadTokens?: number
|
||||
cacheHitRate?: number
|
||||
totalCost: number
|
||||
actualCost?: number
|
||||
successRate: number
|
||||
@@ -47,6 +51,8 @@ export interface ApiFormatStatsItem {
|
||||
api_format: string
|
||||
request_count: number
|
||||
total_tokens: number
|
||||
cache_read_tokens?: number
|
||||
cache_hit_rate?: number
|
||||
total_cost: number
|
||||
actual_cost?: number
|
||||
avgResponseTime: string
|
||||
|
||||
@@ -90,8 +90,8 @@
|
||||
:required="!isEditMode"
|
||||
minlength="6"
|
||||
:placeholder="isEditMode ? '留空保持原密码' : getPasswordPolicyPlaceholder(passwordPolicyLevel)"
|
||||
class="h-10"
|
||||
:class="[
|
||||
'h-10',
|
||||
passwordError ? 'border-destructive' : '',
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -152,4 +152,10 @@ export function formatRemainingTime(expireAt: number | undefined, currentTime: n
|
||||
const minutes = Math.floor(remaining / 60)
|
||||
const seconds = Math.floor(remaining % 60)
|
||||
return `${minutes}分${seconds}秒`
|
||||
}
|
||||
}
|
||||
|
||||
// Cache hit rate formatting
|
||||
export function formatHitRate(rate: number | undefined): string {
|
||||
if (typeof rate !== 'number' || Number.isNaN(rate)) return '-'
|
||||
return `${rate.toFixed(2)}%`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user