Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/api/admin/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export interface AdminUsageStatsResponse {
total_input_tokens: number
total_output_tokens: number
total_cache_tokens: number
total_cache_creation_tokens: number
total_cache_read_tokens: number
total_tokens: number
total_cost: number
total_actual_cost: number
Expand Down
46 changes: 43 additions & 3 deletions frontend/src/components/admin/usage/UsageStatsCards.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,46 @@
<div>
<p class="text-xs font-medium text-gray-500">{{ t('usage.totalTokens') }}</p>
<p class="text-xl font-bold">{{ formatTokens(stats?.total_tokens || 0) }}</p>
<p class="text-xs text-gray-500">
{{ t('usage.in') }}: {{ formatTokens(stats?.total_input_tokens || 0) }} /
{{ t('usage.out') }}: {{ formatTokens(stats?.total_output_tokens || 0) }}
<p class="flex flex-wrap items-center gap-x-1 text-xs text-gray-500">
<span>{{ t('usage.in') }}: {{ formatTokens(stats?.total_input_tokens || 0) }}</span>
<span>/</span>
<span>{{ t('usage.out') }}: {{ formatTokens(stats?.total_output_tokens || 0) }}</span>
<span>/</span>
<span class="group relative inline-flex cursor-help items-center gap-0.5" tabindex="0">
<span>{{ cacheLabel() }}: {{ formatTokens(stats?.total_cache_tokens || 0) }}</span>
<svg
class="h-3.5 w-3.5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span
class="pointer-events-none absolute left-1/2 top-full z-30 mt-2 w-56 -translate-x-1/2 rounded-lg border border-gray-200 bg-white p-3 text-left text-xs text-gray-700 opacity-0 shadow-lg transition-opacity duration-150 group-hover:opacity-100 group-focus:opacity-100 dark:border-dark-600 dark:bg-dark-800 dark:text-dark-200"
>
<span class="mb-2 block font-medium text-gray-900 dark:text-white">
{{ cacheDetailLabel() }}
</span>
<span class="flex items-center justify-between gap-3">
<span>{{ t('usage.cacheCreationTokensLabel') }}</span>
<span class="tabular-nums">
{{ formatTokens(stats?.total_cache_creation_tokens || 0) }}
</span>
</span>
<span class="mt-1 flex items-center justify-between gap-3">
<span>{{ t('usage.cacheReadTokensLabel') }}</span>
<span class="tabular-nums">
{{ formatTokens(stats?.total_cache_read_tokens || 0) }}
</span>
</span>
</span>
</span>
</p>
</div>
</div>
Expand Down Expand Up @@ -64,4 +101,7 @@ const formatTokens = (value: number) => {
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'K'
return value.toLocaleString()
}

const cacheLabel = () => t('usage.cacheTotal')
const cacheDetailLabel = () => t('usage.cacheBreakdown')
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'

import UsageStatsCards from '../UsageStatsCards.vue'

const messages: Record<string, string> = {
'usage.totalRequests': 'Total Requests',
'usage.inSelectedRange': 'in selected range',
'usage.totalTokens': 'Total Tokens',
'usage.in': 'In',
'usage.out': 'Out',
'usage.cacheTotal': 'Cache',
'usage.cacheBreakdown': 'Cache Token Breakdown',
'usage.cacheCreationTokensLabel': 'Cache Creation',
'usage.cacheReadTokensLabel': 'Cache Read',
'usage.totalCost': 'Total Cost',
'usage.accountCost': 'Cost',
'usage.standardCost': 'Standard',
'usage.avgDuration': 'Avg Duration',
}

vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({
t: (key: string) => messages[key] ?? key,
}),
}
})

const stats = {
total_requests: 1,
total_input_tokens: 100,
total_output_tokens: 50,
total_cache_tokens: 34,
total_cache_creation_tokens: 12,
total_cache_read_tokens: 22,
total_tokens: 184,
total_cost: 0.001,
total_actual_cost: 0.001,
total_account_cost: 0.001,
average_duration_ms: 250,
}

describe('UsageStatsCards', () => {
it('shows cache token breakdown values', () => {
const wrapper = mount(UsageStatsCards, {
props: {
stats,
},
global: {
stubs: {
Icon: true,
},
},
})

const text = wrapper.text()
expect(text).toContain('Cache: 34')
expect(text).toContain('Cache Token Breakdown')
expect(text).toContain('Cache Creation')
expect(text).toContain('12')
expect(text).toContain('Cache Read')
expect(text).toContain('22')
})
})
4 changes: 4 additions & 0 deletions frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,10 @@ export default {
cacheTtlOverridden1h: 'Billed as 1h',
totalRequests: 'Total Requests',
totalTokens: 'Total Tokens',
cacheTotal: 'Cache',
cacheBreakdown: 'Cache Token Breakdown',
cacheCreationTokensLabel: 'Cache Creation',
cacheReadTokensLabel: 'Cache Read',
totalCost: 'Total Cost',
standardCost: 'Standard',
actualCost: 'Actual',
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,10 @@ export default {
cacheTtlOverridden1h: '按 1h 计费',
totalRequests: '总请求数',
totalTokens: '总 Token',
cacheTotal: '缓存',
cacheBreakdown: '缓存 Token 明细',
cacheCreationTokensLabel: '缓存创建',
cacheReadTokensLabel: '缓存读取',
totalCost: '总消费',
standardCost: '标准',
actualCost: '实际',
Expand Down
Loading