Add all-time usage statistics tab
Extends the existing Stats sheet (⌘⇧S) with a Session/All-Time segmented picker. All-Time aggregates tokens, cost, and message counts across every saved conversation, broken down by model and by conversation, using the modelId already stored per message — no schema change needed.
This commit is contained in:
@@ -121,3 +121,102 @@ struct DatabaseServiceConversationTests {
|
||||
#expect(loaded?.1.first?.content == "hello")
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("DatabaseService usage statistics, against a throwaway in-memory queue")
|
||||
struct DatabaseServiceUsageStatsTests {
|
||||
|
||||
@Test("Overall stats aggregate tokens, cost, and message count across conversations")
|
||||
func overallStatsAggregate() throws {
|
||||
let db = DatabaseService.makeInMemory()
|
||||
_ = try db.saveConversation(name: "Chat A", messages: [
|
||||
Message(role: .user, content: "hi", tokens: 10, modelId: "claude-sonnet"),
|
||||
Message(role: .assistant, content: "hello", tokens: 20, cost: 0.01, modelId: "claude-sonnet"),
|
||||
])
|
||||
_ = try db.saveConversation(name: "Chat B", messages: [
|
||||
Message(role: .user, content: "hey", tokens: 5, modelId: "gpt-4"),
|
||||
Message(role: .assistant, content: "hi", tokens: 15, cost: 0.02, modelId: "gpt-4"),
|
||||
])
|
||||
|
||||
let stats = try db.getOverallUsageStats()
|
||||
#expect(stats.totalMessages == 4)
|
||||
#expect(stats.totalTokens == 50)
|
||||
#expect(stats.hasCostData == true)
|
||||
#expect(abs(stats.totalCost - 0.03) < 0.0001)
|
||||
}
|
||||
|
||||
@Test("Overall stats report no cost data when no message has a cost")
|
||||
func overallStatsNoCostData() throws {
|
||||
let db = DatabaseService.makeInMemory()
|
||||
_ = try db.saveConversation(name: "Chat", messages: [
|
||||
Message(role: .user, content: "hi", tokens: 10),
|
||||
])
|
||||
|
||||
let stats = try db.getOverallUsageStats()
|
||||
#expect(stats.hasCostData == false)
|
||||
#expect(stats.totalCost == 0)
|
||||
}
|
||||
|
||||
@Test("Usage by model groups messages by modelId and sums their tokens/cost")
|
||||
func usageByModelGroups() throws {
|
||||
let db = DatabaseService.makeInMemory()
|
||||
_ = try db.saveConversation(name: "Chat A", messages: [
|
||||
Message(role: .assistant, content: "a1", tokens: 20, cost: 0.01, modelId: "claude-sonnet"),
|
||||
])
|
||||
_ = try db.saveConversation(name: "Chat B", messages: [
|
||||
Message(role: .assistant, content: "b1", tokens: 15, cost: 0.02, modelId: "gpt-4"),
|
||||
Message(role: .assistant, content: "b2", tokens: 5, cost: 0.02, modelId: "gpt-4"),
|
||||
])
|
||||
|
||||
let byModel = try db.getUsageByModel()
|
||||
#expect(byModel.count == 2)
|
||||
|
||||
let gpt4 = byModel.first { $0.modelId == "gpt-4" }
|
||||
#expect(gpt4?.messageCount == 2)
|
||||
#expect(gpt4?.totalTokens == 20)
|
||||
#expect(abs((gpt4?.totalCost ?? 0) - 0.04) < 0.0001)
|
||||
|
||||
// gpt-4 has higher total cost than claude-sonnet, so it should sort first
|
||||
#expect(byModel.first?.modelId == "gpt-4")
|
||||
}
|
||||
|
||||
@Test("Usage by model excludes messages with no modelId")
|
||||
func usageByModelExcludesNilModelId() throws {
|
||||
let db = DatabaseService.makeInMemory()
|
||||
_ = try db.saveConversation(name: "Chat", messages: [
|
||||
Message(role: .user, content: "hi", tokens: 10),
|
||||
Message(role: .assistant, content: "hello", tokens: 20, modelId: "claude-sonnet"),
|
||||
])
|
||||
|
||||
let byModel = try db.getUsageByModel()
|
||||
#expect(byModel.count == 1)
|
||||
#expect(byModel.first?.modelId == "claude-sonnet")
|
||||
}
|
||||
|
||||
@Test("Usage by conversation joins conversation names and sorts by cost descending")
|
||||
func usageByConversationSortsByCost() throws {
|
||||
let db = DatabaseService.makeInMemory()
|
||||
_ = try db.saveConversation(name: "Cheap Chat", messages: [
|
||||
Message(role: .assistant, content: "a", tokens: 10, cost: 0.001, modelId: "m"),
|
||||
])
|
||||
_ = try db.saveConversation(name: "Expensive Chat", messages: [
|
||||
Message(role: .assistant, content: "b", tokens: 10, cost: 0.05, modelId: "m"),
|
||||
])
|
||||
|
||||
let byConversation = try db.getUsageByConversation()
|
||||
#expect(byConversation.count == 2)
|
||||
#expect(byConversation.first?.name == "Expensive Chat")
|
||||
}
|
||||
|
||||
@Test("Usage by conversation respects the limit parameter")
|
||||
func usageByConversationRespectsLimit() throws {
|
||||
let db = DatabaseService.makeInMemory()
|
||||
for i in 0..<5 {
|
||||
_ = try db.saveConversation(name: "Chat \(i)", messages: [
|
||||
Message(role: .assistant, content: "a", tokens: 10, cost: Double(i) * 0.01, modelId: "m"),
|
||||
])
|
||||
}
|
||||
|
||||
let byConversation = try db.getUsageByConversation(limit: 3)
|
||||
#expect(byConversation.count == 3)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user