1package soy.iko.opencode.ui.usage
3import androidx.compose.runtime.Immutable
4import soy.iko.opencode.data.model.AssistantMessage
5import soy.iko.opencode.data.model.MessageWithParts
6import soy.iko.opencode.data.model.Tokens
8/** Aggregated usage for one model across the scanned sessions. */
18/** Aggregated usage for one session. */
20data class SessionUsage(
21 val sessionId: String,
28/** A full usage report: overall totals plus per-model and per-session breakdowns. */
30data class UsageReport(
31 val totalCost: Double = 0.0,
32 val totalTokens: Tokens = Tokens(),
33 val messageCount: Int = 0,
34 val sessionCount: Int = 0,
35 val byModel: List<ModelUsage> = emptyList(),
36 val bySession: List<SessionUsage> = emptyList(),
38 val isEmpty: Boolean get() = messageCount == 0
41/** Sum of two token counters (including the cache read/write sub-counters). */
42internal operator fun Tokens.plus(other: Tokens): Tokens = Tokens(
43 input = input + other.input,
44 output = output + other.output,
45 reasoning = reasoning + other.reasoning,
46 cache = Tokens.Cache(read = cache.read + other.cache.read, write = cache.write + other.cache.write),
49/** Total tokens across all sub-counters, for a single "N tokens" headline. */
50internal val Tokens.total: Long get() = input + output + reasoning + cache.read + cache.write
52private class MutableAgg(val provider: String, val model: String) {
59 * Aggregate cost/token usage across sessions. Each entry is (sessionId, title, messages).
60 * Only [AssistantMessage]s that reported cost or tokens contribute — user prompts and
61 * zero-usage messages are ignored so the counts reflect real model spend. Pure and
62 * synchronous so it's unit-testable and can run off the main thread.
64fun aggregateUsage(sessions: List<Triple<String, String, List<MessageWithParts>>>): UsageReport {
66 var totalTokens = Tokens()
68 val byModel = LinkedHashMap<String, MutableAgg>()
69 val bySession = ArrayList<SessionUsage>()
71 for ((id, title, messages) in sessions) {
73 var sessionTokens = Tokens()
74 var sessionMessages = 0
76 val info = m.info as? AssistantMessage ?: continue
77 val cost = info.cost ?: 0.0
78 val tokens = info.tokens ?: Tokens()
79 if (cost == 0.0 && tokens.total == 0L) continue
85 sessionTokens += tokens
86 val model = info.modelID?.takeIf { it.isNotBlank() } ?: "unknown"
87 val provider = info.providerID?.takeIf { it.isNotBlank() } ?: "unknown"
88 // Key by provider+model so the same model id served by different providers
89 // stays two rows instead of collapsing into one ambiguous total.
90 val key = "$provider/$model"
91 val agg = byModel.getOrPut(key) { MutableAgg(provider, model) }
96 if (sessionMessages > 0) {
97 bySession.add(SessionUsage(id, title, sessionMessages, sessionCost, sessionTokens))
102 totalCost = totalCost,
103 totalTokens = totalTokens,
104 messageCount = messageCount,
105 sessionCount = bySession.size,
106 byModel = byModel.entries
107 .map { ModelUsage(it.value.provider, it.value.model, it.value.messages, it.value.cost, it.value.tokens) }
108 .sortedByDescending { it.cost },
109 bySession = bySession.sortedByDescending { it.cost },
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list