Files
Hermes-ui/src/stores/usage.ts
T
ekko 9dd5fca9f9 feat: add usage statistics page, CLI improvements, and UI enhancements
- Add Usage Stats page with token breakdown, model distribution, and 30-day trend
- Pass through cache/cost token fields in BFF (cache_read/write_tokens, reasoning_tokens, actual_cost_usd)
- Add CLI commands: -v/--version, -h/--help, update/upgrade with auto-restart
- Auto-open browser on startup, auto-kill port conflicts (cross-platform)
- Validate all api_server config fields on startup (enabled, host, port, key, cors_origins)
- Add streaming thinking video animation with tool calls panel
- Add context token usage display (used / total) in chat header
- Sidebar: white logo area with shadow, dance video beside logo (canvas seamless loop)
- Fix sidebar nav scroll (app-main overflow-y: auto)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 14:48:58 +08:00

142 lines
3.8 KiB
TypeScript

import { fetchSessions, type SessionSummary } from '@/api/sessions'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
interface DailyUsage {
date: string
tokens: number
cache: number
sessions: number
cost: number
}
interface ModelUsage {
model: string
inputTokens: number
outputTokens: number
cacheTokens: number
totalTokens: number
sessions: number
}
export const useUsageStore = defineStore('usage', () => {
const sessions = ref<SessionSummary[]>([])
const isLoading = ref(false)
async function loadSessions() {
isLoading.value = true
try {
sessions.value = await fetchSessions()
} catch (err) {
console.error('Failed to load sessions for usage:', err)
} finally {
isLoading.value = false
}
}
const totalInputTokens = computed(() =>
sessions.value.reduce((sum, s) => sum + (s.input_tokens || 0), 0),
)
const totalOutputTokens = computed(() =>
sessions.value.reduce((sum, s) => sum + (s.output_tokens || 0), 0),
)
const totalTokens = computed(() => totalInputTokens.value + totalOutputTokens.value)
const totalSessions = computed(() => sessions.value.length)
const totalCacheTokens = computed(() =>
sessions.value.reduce((sum, s) => sum + (s.cache_read_tokens || 0), 0),
)
const cacheHitRate = computed(() => {
const total = totalInputTokens.value
if (total === 0) return null
return ((totalCacheTokens.value / total) * 100)
})
const estimatedCost = computed(() =>
sessions.value.reduce((sum, s) => {
const cost = s.actual_cost_usd ?? s.estimated_cost_usd ?? 0
return sum + cost
}, 0),
)
const modelUsage = computed<ModelUsage[]>(() => {
const map = new Map<string, ModelUsage>()
for (const s of sessions.value) {
const key = s.model || 'unknown'
if (!map.has(key)) {
map.set(key, {
model: key,
inputTokens: 0,
outputTokens: 0,
cacheTokens: 0,
totalTokens: 0,
sessions: 0,
})
}
const entry = map.get(key)!
entry.inputTokens += s.input_tokens || 0
entry.outputTokens += s.output_tokens || 0
entry.cacheTokens += s.cache_read_tokens || 0
entry.totalTokens += (s.input_tokens || 0) + (s.output_tokens || 0)
entry.sessions += 1
}
return [...map.values()].sort((a, b) => b.totalTokens - a.totalTokens)
})
const dailyUsage = computed<DailyUsage[]>(() => {
const map = new Map<string, DailyUsage>()
const now = new Date()
// Initialize last 30 days
for (let i = 29; i >= 0; i--) {
const d = new Date(now)
d.setDate(d.getDate() - i)
const key = d.toISOString().slice(0, 10)
map.set(key, { date: key, tokens: 0, cache: 0, sessions: 0, cost: 0 })
}
for (const s of sessions.value) {
const d = new Date(s.started_at * 1000)
const key = d.toISOString().slice(0, 10)
const entry = map.get(key)
if (entry) {
entry.tokens += (s.input_tokens || 0) + (s.output_tokens || 0)
entry.cache += s.cache_read_tokens || 0
entry.sessions += 1
const cost = s.actual_cost_usd ?? s.estimated_cost_usd ?? 0
entry.cost += cost
}
}
return [...map.values()]
})
const avgSessionsPerDay = computed(() => {
const firstDate = sessions.value.length > 0
? new Date(sessions.value[sessions.value.length - 1].started_at * 1000)
: new Date()
const days = Math.max(1, Math.ceil((Date.now() - firstDate.getTime()) / (1000 * 60 * 60 * 24)))
return totalSessions.value / days
})
return {
sessions,
isLoading,
loadSessions,
totalInputTokens,
totalOutputTokens,
totalTokens,
totalSessions,
totalCacheTokens,
cacheHitRate,
estimatedCost,
modelUsage,
dailyUsage,
avgSessionsPerDay,
}
})