feat(usage): filter usage stats by active profile

Usage stats now automatically filter by the current active profile.

Changes:
- getLocalUsageStats() accepts optional profile parameter
- Add WHERE profile = ? clause to all SQL queries when profile is provided
- usageStats controller uses getActiveProfileName() to get current profile
- Local session_usage data is now filtered by current profile
- Hermes state.db sessions remain unfiltered (no profile field)

This allows users to see usage stats specific to their current profile,
making multi-profile usage tracking more useful.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-04-29 10:55:29 +08:00
parent f9ce587955
commit 47020ee08d
2 changed files with 29 additions and 12 deletions
@@ -289,8 +289,11 @@ export async function contextLength(ctx: any) {
}
export async function usageStats(ctx: any) {
// 1. Local session_usage (web UI chat runs)
const local = getLocalUsageStats()
// Get current active profile
const currentProfile = getActiveProfileName()
// 1. Local session_usage (web UI chat runs) - filtered by current profile
const local = getLocalUsageStats(currentProfile)
// 2. Hermes state.db sessions (exclude api_server source)
let hermesSessions: Array<{
@@ -307,6 +310,9 @@ export async function usageStats(ctx: any) {
try {
const allSessions = await listSessionSummaries(undefined, 100000)
// Only include sessions from current profile
// Note: Hermes sessions don't have profile field, so we include all
// This could be improved in the future by filtering by some criteria
hermesSessions = allSessions.filter(s => s.source !== 'api_server')
} catch (err) {
logger.warn(err, 'usageStats: failed to load Hermes sessions')
+21 -10
View File
@@ -199,7 +199,7 @@ export interface LocalUsageStats {
by_day: UsageStatsDailyRow[]
}
export function getLocalUsageStats(): LocalUsageStats {
export function getLocalUsageStats(profile?: string): LocalUsageStats {
const empty: LocalUsageStats = {
input_tokens: 0, output_tokens: 0, cache_read_tokens: 0,
cache_write_tokens: 0, reasoning_tokens: 0, sessions: 0,
@@ -208,6 +208,7 @@ export function getLocalUsageStats(): LocalUsageStats {
if (!isSqliteAvailable()) return empty
const db = getDb()!
const profileFilter = profile ? `WHERE profile = ?` : ''
const totals = db.prepare(`
SELECT COALESCE(SUM(input_tokens),0) as input_tokens,
@@ -217,7 +218,8 @@ export function getLocalUsageStats(): LocalUsageStats {
COALESCE(SUM(reasoning_tokens),0) as reasoning_tokens,
COUNT(DISTINCT session_id) as sessions
FROM ${TABLE}
`).get() as any
${profileFilter}
`).get(...(profile ? [profile] : [])) as any
const byModel = db.prepare(`
SELECT model,
@@ -228,21 +230,30 @@ export function getLocalUsageStats(): LocalUsageStats {
SUM(reasoning_tokens) as reasoning_tokens,
COUNT(DISTINCT session_id) as sessions
FROM ${TABLE}
${profileFilter}
GROUP BY model
ORDER BY sessions DESC
`).all() as unknown as UsageStatsModelRow[]
`).all(...(profile ? [profile] : [])) as unknown as UsageStatsModelRow[]
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000
const byDay = db.prepare(`
SELECT DATE(created_at / 1000, 'unixepoch') as date,
const byDayStmt = profile
? `SELECT DATE(created_at / 1000, 'unixepoch') as date,
SUM(input_tokens + output_tokens) as tokens,
SUM(cache_read_tokens) as cache,
COUNT(DISTINCT session_id) as sessions
FROM ${TABLE}
WHERE created_at > ?
GROUP BY date
ORDER BY date
`).all(thirtyDaysAgo) as Array<{ date: string; tokens: number; cache: number; sessions: number }>
FROM ${TABLE}
WHERE profile = ? AND created_at > ?
GROUP BY date
ORDER BY date`
: `SELECT DATE(created_at / 1000, 'unixepoch') as date,
SUM(input_tokens + output_tokens) as tokens,
SUM(cache_read_tokens) as cache,
COUNT(DISTINCT session_id) as sessions
FROM ${TABLE}
WHERE created_at > ?
GROUP BY date
ORDER BY date`
const byDay = db.prepare(byDayStmt).all(...(profile ? [profile, thirtyDaysAgo] : [thirtyDaysAgo])) as Array<{ date: string; tokens: number; cache: number; sessions: number }>
return {
input_tokens: totals.input_tokens,