2026-04-29 16:26:24 +08:00
|
|
|
import { fetchUsageStats, type UsageStatsResponse } from '@/api/hermes/sessions'
|
2026-04-14 14:47:18 +08:00
|
|
|
import { defineStore } from 'pinia'
|
|
|
|
|
import { computed, ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
interface DailyUsage {
|
|
|
|
|
date: string
|
2026-05-02 10:36:33 +10:00
|
|
|
input_tokens: number
|
|
|
|
|
output_tokens: number
|
|
|
|
|
cache_read_tokens: number
|
|
|
|
|
cache_write_tokens: number
|
2026-04-14 14:47:18 +08:00
|
|
|
sessions: number
|
2026-05-02 10:36:33 +10:00
|
|
|
errors: number
|
2026-04-14 14:47:18 +08:00
|
|
|
cost: number
|
2026-05-13 01:41:49 +02:00
|
|
|
visualTokens: number
|
|
|
|
|
inputPercent: number
|
|
|
|
|
outputPercent: number
|
|
|
|
|
cachePercent: number
|
2026-04-14 14:47:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ModelUsage {
|
|
|
|
|
model: string
|
|
|
|
|
inputTokens: number
|
|
|
|
|
outputTokens: number
|
|
|
|
|
cacheTokens: number
|
2026-05-13 01:41:49 +02:00
|
|
|
cacheWriteTokens: number
|
2026-04-14 14:47:18 +08:00
|
|
|
totalTokens: number
|
2026-05-13 01:41:49 +02:00
|
|
|
visualTokens: number
|
2026-04-14 14:47:18 +08:00
|
|
|
sessions: number
|
2026-05-13 01:41:49 +02:00
|
|
|
color: string
|
|
|
|
|
inputPercent: number
|
|
|
|
|
outputPercent: number
|
|
|
|
|
cachePercent: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const MODEL_COLORS = [
|
|
|
|
|
'#4fd1c5',
|
|
|
|
|
'#63b3ed',
|
|
|
|
|
'#f6ad55',
|
|
|
|
|
'#b794f4',
|
|
|
|
|
'#68d391',
|
|
|
|
|
'#fc8181',
|
|
|
|
|
'#f687b3',
|
|
|
|
|
'#90cdf4',
|
|
|
|
|
'#fbd38d',
|
|
|
|
|
'#9ae6b4',
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function normalizeModel(model: string | null | undefined): string {
|
|
|
|
|
const trimmed = (model || '').trim()
|
|
|
|
|
return trimmed || 'unknown'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function percent(part: number, total: number): number {
|
|
|
|
|
if (total <= 0) return 0
|
|
|
|
|
return part / total * 100
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getModelColor(model: string): string {
|
|
|
|
|
const normalized = normalizeModel(model)
|
|
|
|
|
let hash = 0
|
|
|
|
|
for (let i = 0; i < normalized.length; i += 1) {
|
|
|
|
|
hash = ((hash << 5) - hash) + normalized.charCodeAt(i)
|
|
|
|
|
hash |= 0
|
|
|
|
|
}
|
|
|
|
|
return MODEL_COLORS[Math.abs(hash) % MODEL_COLORS.length]
|
2026-04-14 14:47:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const useUsageStore = defineStore('usage', () => {
|
2026-04-29 16:26:24 +08:00
|
|
|
const stats = ref<UsageStatsResponse | null>(null)
|
2026-04-14 14:47:18 +08:00
|
|
|
const isLoading = ref(false)
|
2026-05-13 01:41:49 +02:00
|
|
|
let latestRequestId = 0
|
2026-04-14 14:47:18 +08:00
|
|
|
|
2026-04-30 13:46:31 +02:00
|
|
|
async function loadSessions(days = 30) {
|
2026-05-13 01:41:49 +02:00
|
|
|
const requestId = ++latestRequestId
|
2026-04-14 14:47:18 +08:00
|
|
|
isLoading.value = true
|
|
|
|
|
try {
|
2026-05-13 01:41:49 +02:00
|
|
|
const response = await fetchUsageStats(days)
|
|
|
|
|
if (requestId === latestRequestId) {
|
|
|
|
|
stats.value = response
|
|
|
|
|
}
|
2026-04-14 14:47:18 +08:00
|
|
|
} catch (err) {
|
2026-05-13 01:41:49 +02:00
|
|
|
if (requestId === latestRequestId) {
|
|
|
|
|
console.error('Failed to load usage stats:', err)
|
|
|
|
|
}
|
2026-04-14 14:47:18 +08:00
|
|
|
} finally {
|
2026-05-13 01:41:49 +02:00
|
|
|
if (requestId === latestRequestId) {
|
|
|
|
|
isLoading.value = false
|
|
|
|
|
}
|
2026-04-14 14:47:18 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 16:26:24 +08:00
|
|
|
const hasData = computed(() => !!stats.value && stats.value.total_sessions > 0)
|
2026-04-14 14:47:18 +08:00
|
|
|
|
2026-04-29 16:26:24 +08:00
|
|
|
const totalInputTokens = computed(() => stats.value?.total_input_tokens ?? 0)
|
|
|
|
|
const totalOutputTokens = computed(() => stats.value?.total_output_tokens ?? 0)
|
2026-04-14 14:47:18 +08:00
|
|
|
const totalTokens = computed(() => totalInputTokens.value + totalOutputTokens.value)
|
2026-04-29 16:26:24 +08:00
|
|
|
const totalSessions = computed(() => stats.value?.total_sessions ?? 0)
|
2026-04-14 14:47:18 +08:00
|
|
|
|
2026-04-29 16:26:24 +08:00
|
|
|
const totalCacheTokens = computed(() => stats.value?.total_cache_read_tokens ?? 0)
|
2026-04-14 14:47:18 +08:00
|
|
|
|
|
|
|
|
const cacheHitRate = computed(() => {
|
2026-04-29 16:26:24 +08:00
|
|
|
const total = totalInputTokens.value + totalCacheTokens.value
|
2026-04-14 14:47:18 +08:00
|
|
|
if (total === 0) return null
|
|
|
|
|
return ((totalCacheTokens.value / total) * 100)
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-29 16:26:24 +08:00
|
|
|
const estimatedCost = computed(() => stats.value?.total_cost ?? 0)
|
2026-04-14 14:47:18 +08:00
|
|
|
|
|
|
|
|
const modelUsage = computed<ModelUsage[]>(() => {
|
2026-04-29 16:26:24 +08:00
|
|
|
if (!stats.value) return []
|
2026-05-13 01:41:49 +02:00
|
|
|
return stats.value.model_usage.map(m => {
|
|
|
|
|
const model = normalizeModel(m.model)
|
|
|
|
|
const totalTokens = m.input_tokens + m.output_tokens
|
|
|
|
|
const visualTokens = totalTokens + m.cache_read_tokens
|
|
|
|
|
return {
|
|
|
|
|
model,
|
|
|
|
|
inputTokens: m.input_tokens,
|
|
|
|
|
outputTokens: m.output_tokens,
|
|
|
|
|
cacheTokens: m.cache_read_tokens,
|
|
|
|
|
cacheWriteTokens: m.cache_write_tokens,
|
|
|
|
|
totalTokens,
|
|
|
|
|
visualTokens,
|
|
|
|
|
sessions: m.sessions,
|
|
|
|
|
color: getModelColor(model),
|
|
|
|
|
inputPercent: percent(m.input_tokens, visualTokens),
|
|
|
|
|
outputPercent: percent(m.output_tokens, visualTokens),
|
|
|
|
|
cachePercent: percent(m.cache_read_tokens, visualTokens),
|
|
|
|
|
}
|
|
|
|
|
}).sort((a, b) => b.visualTokens - a.visualTokens)
|
2026-04-14 14:47:18 +08:00
|
|
|
})
|
|
|
|
|
|
2026-05-13 01:41:49 +02:00
|
|
|
const modelLegend = computed(() => {
|
|
|
|
|
const seen = new Set<string>()
|
|
|
|
|
return modelUsage.value.filter(m => {
|
|
|
|
|
if (seen.has(m.model)) return false
|
|
|
|
|
seen.add(m.model)
|
|
|
|
|
return true
|
|
|
|
|
}).map(m => ({ model: m.model, color: m.color }))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const dailyUsage = computed<DailyUsage[]>(() => (stats.value?.daily_usage ?? []).map(d => {
|
|
|
|
|
const visualTokens = d.input_tokens + d.output_tokens + d.cache_read_tokens
|
|
|
|
|
return {
|
|
|
|
|
...d,
|
|
|
|
|
visualTokens,
|
|
|
|
|
inputPercent: percent(d.input_tokens, visualTokens),
|
|
|
|
|
outputPercent: percent(d.output_tokens, visualTokens),
|
|
|
|
|
cachePercent: percent(d.cache_read_tokens, visualTokens),
|
|
|
|
|
}
|
|
|
|
|
}))
|
2026-04-14 14:47:18 +08:00
|
|
|
|
|
|
|
|
const avgSessionsPerDay = computed(() => {
|
2026-04-29 16:26:24 +08:00
|
|
|
if (!stats.value || stats.value.daily_usage.length === 0) return 0
|
|
|
|
|
const daysWithActivity = stats.value.daily_usage.filter(d => d.sessions > 0).length
|
|
|
|
|
const days = Math.max(1, daysWithActivity)
|
2026-04-14 14:47:18 +08:00
|
|
|
return totalSessions.value / days
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
2026-04-29 16:26:24 +08:00
|
|
|
stats,
|
2026-04-14 14:47:18 +08:00
|
|
|
isLoading,
|
2026-04-29 16:26:24 +08:00
|
|
|
hasData,
|
2026-04-14 14:47:18 +08:00
|
|
|
loadSessions,
|
|
|
|
|
totalInputTokens,
|
|
|
|
|
totalOutputTokens,
|
|
|
|
|
totalTokens,
|
|
|
|
|
totalSessions,
|
|
|
|
|
totalCacheTokens,
|
|
|
|
|
cacheHitRate,
|
|
|
|
|
estimatedCost,
|
|
|
|
|
modelUsage,
|
2026-05-13 01:41:49 +02:00
|
|
|
modelLegend,
|
2026-04-14 14:47:18 +08:00
|
|
|
dailyUsage,
|
|
|
|
|
avgSessionsPerDay,
|
2026-05-13 01:41:49 +02:00
|
|
|
getModelColor,
|
2026-04-14 14:47:18 +08:00
|
|
|
}
|
|
|
|
|
})
|