Skip to content

Commit 2f1a936

Browse files
committed
feat(llm): add token usage service with session tracking
Create TokenUsageService to centrally manage token statistics across sessions with availability calculations, and refactor TokenUsagePanel to use cleaner UI layout and improved progress visualization.
1 parent 8a917d8 commit 2f1a936

File tree

2 files changed

+135
-32
lines changed

2 files changed

+135
-32
lines changed

core/src/main/kotlin/cc/unitmesh/devti/gui/chat/ui/TokenUsagePanel.kt

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import com.intellij.util.ui.JBUI
1313
import com.intellij.util.ui.UIUtil
1414
import com.intellij.util.ui.components.BorderLayoutPanel
1515
import java.awt.BorderLayout
16-
import java.awt.Color
1716
import java.awt.Font
1817
import java.awt.GridBagConstraints
1918
import java.awt.GridBagLayout
@@ -25,10 +24,6 @@ import javax.swing.SwingConstants
2524
* Panel that displays token usage statistics for the current session
2625
*/
2726
class TokenUsagePanel(private val project: Project) : BorderLayoutPanel() {
28-
29-
private val promptTokensLabel = JBLabel("0", SwingConstants.RIGHT)
30-
private val completionTokensLabel = JBLabel("0", SwingConstants.RIGHT)
31-
private val totalTokensLabel = JBLabel("0", SwingConstants.RIGHT)
3227
private val modelLabel = JBLabel("", SwingConstants.LEFT)
3328
private val progressBar = JProgressBar(0, 100)
3429
private val usageRatioLabel = JBLabel("", SwingConstants.CENTER)
@@ -46,7 +41,6 @@ class TokenUsagePanel(private val project: Project) : BorderLayoutPanel() {
4641
isOpaque = false
4742
border = JBUI.Borders.empty(4, 8)
4843

49-
// Setup progress bar
5044
progressBar.apply {
5145
isStringPainted = false
5246
preferredSize = java.awt.Dimension(150, 8)
@@ -55,19 +49,16 @@ class TokenUsagePanel(private val project: Project) : BorderLayoutPanel() {
5549
isOpaque = false
5650
}
5751

58-
// Setup usage ratio label
5952
usageRatioLabel.apply {
6053
font = font.deriveFont(Font.PLAIN, 10f)
6154
foreground = UIUtil.getContextHelpForeground()
6255
}
6356

64-
// Create main layout
6557
val mainPanel = JPanel(GridBagLayout())
6658
mainPanel.isOpaque = false
6759

6860
val gbc = GridBagConstraints()
6961

70-
// Top row: Model info and progress bar
7162
gbc.gridx = 0
7263
gbc.gridy = 0
7364
gbc.anchor = GridBagConstraints.WEST
@@ -129,18 +120,13 @@ class TokenUsagePanel(private val project: Project) : BorderLayoutPanel() {
129120
currentUsage = event.usage
130121
currentModel = event.model
131122

132-
// Get max tokens for current model
133123
updateMaxTokens()
134-
135-
136-
// Update progress bar
137124
updateProgressBar(event.usage.totalTokens ?: 0)
138-
125+
139126
if (!event.model.isNullOrBlank()) {
140127
modelLabel.text = "Model: ${event.model}"
141128
}
142129

143-
// Show the panel when we have data
144130
isVisible = true
145131
revalidate()
146132
repaint()
@@ -154,7 +140,6 @@ class TokenUsagePanel(private val project: Project) : BorderLayoutPanel() {
154140
val limits = modelManager.getUsedMaxToken()
155141
maxContextWindowTokens = limits.maxContextWindowTokens?.toLong() ?: 0
156142
} catch (e: Exception) {
157-
// Fallback to default if unable to get limits
158143
maxContextWindowTokens = 4096
159144
}
160145
}
@@ -169,7 +154,6 @@ class TokenUsagePanel(private val project: Project) : BorderLayoutPanel() {
169154
val usageRatio = (totalTokens.toDouble() / maxContextWindowTokens * 100).toInt()
170155
progressBar.value = usageRatio.coerceIn(0, 100)
171156

172-
// Update color based on usage ratio
173157
progressBar.foreground = when {
174158
usageRatio >= 90 -> JBColor.RED
175159
usageRatio >= 75 -> JBColor.ORANGE
@@ -178,12 +162,9 @@ class TokenUsagePanel(private val project: Project) : BorderLayoutPanel() {
178162
else -> UIUtil.getPanelBackground().brighter()
179163
}
180164

181-
// Update ratio label
182165
usageRatioLabel.text = "${formatTokenCount(totalTokens)}/${formatTokenCount(maxContextWindowTokens)} (${usageRatio}%)"
183-
184166
progressBar.isVisible = true
185167
usageRatioLabel.isVisible = true
186-
187168
progressBar.toolTipText = "Token usage: $usageRatio% of context window"
188169
}
189170

@@ -194,10 +175,7 @@ class TokenUsagePanel(private val project: Project) : BorderLayoutPanel() {
194175
else -> count.toString()
195176
}
196177
}
197-
198-
/**
199-
* Reset the token usage display
200-
*/
178+
201179
fun reset() {
202180
ApplicationManager.getApplication().invokeLater {
203181
currentUsage = Usage()
@@ -213,14 +191,8 @@ class TokenUsagePanel(private val project: Project) : BorderLayoutPanel() {
213191
repaint()
214192
}
215193
}
216-
217-
/**
218-
* Get current usage for external access
219-
*/
194+
220195
fun getCurrentUsage(): Usage = currentUsage
221-
222-
/**
223-
* Get current model for external access
224-
*/
196+
225197
fun getCurrentModel(): String? = currentModel
226198
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package cc.unitmesh.devti.llm2
2+
3+
import cc.unitmesh.devti.llms.custom.Usage
4+
import cc.unitmesh.devti.settings.model.LLMModelManager
5+
import com.intellij.openapi.application.ApplicationManager
6+
import com.intellij.openapi.components.Service
7+
import com.intellij.openapi.project.Project
8+
import com.intellij.util.messages.MessageBusConnection
9+
import java.util.concurrent.ConcurrentHashMap
10+
import java.util.concurrent.atomic.AtomicLong
11+
12+
/**
13+
* Service for managing token usage statistics and calculations
14+
*/
15+
@Service(Service.Level.PROJECT)
16+
class TokenUsageService(private val project: Project) : TokenUsageListener {
17+
private val sessionTokenUsage = ConcurrentHashMap<String, Usage>()
18+
private val totalTokenUsage = AtomicLong(0)
19+
private val totalPromptTokens = AtomicLong(0)
20+
private val totalCompletionTokens = AtomicLong(0)
21+
22+
private var currentSessionId: String? = null
23+
private var currentModel: String? = null
24+
private var messageBusConnection: MessageBusConnection? = null
25+
26+
init {
27+
setupTokenUsageListener()
28+
}
29+
30+
private fun setupTokenUsageListener() {
31+
messageBusConnection = ApplicationManager.getApplication().messageBus.connect()
32+
messageBusConnection?.subscribe(TokenUsageListener.TOPIC, this)
33+
}
34+
35+
override fun onTokenUsage(event: TokenUsageEvent) {
36+
event.sessionId?.let { sessionId ->
37+
sessionTokenUsage[sessionId] = event.usage
38+
currentSessionId = sessionId
39+
}
40+
41+
// Update current model
42+
currentModel = event.model
43+
44+
// Update total usage statistics
45+
totalTokenUsage.addAndGet(event.usage.totalTokens)
46+
totalPromptTokens.addAndGet(event.usage.promptTokens)
47+
totalCompletionTokens.addAndGet(event.usage.completionTokens)
48+
}
49+
50+
/**
51+
* Get the current consumed tokens for the active session
52+
*/
53+
fun getCurrentConsumedTokens(): Usage {
54+
return currentSessionId?.let { sessionId ->
55+
sessionTokenUsage[sessionId]
56+
} ?: Usage()
57+
}
58+
59+
/**
60+
* Get total consumed tokens across all sessions
61+
*/
62+
fun getTotalConsumedTokens(): Usage {
63+
return Usage(
64+
promptTokens = totalPromptTokens.get(),
65+
completionTokens = totalCompletionTokens.get(),
66+
totalTokens = totalTokenUsage.get()
67+
)
68+
}
69+
70+
/**
71+
* Get the maximum tokens allowed for the current model
72+
*/
73+
fun getUsedMaxToken(): Long {
74+
return LLMModelManager.getInstance().getUsedMaxToken().maxContextWindowTokens?.toLong() ?: 0L
75+
}
76+
77+
/**
78+
* Calculate token availability ratio (0.0 to 1.0)
79+
* Returns the percentage of tokens still available
80+
*/
81+
fun calculateTokenAvailability(): Double {
82+
val maxTokens = getUsedMaxToken()
83+
if (maxTokens <= 0) return 1.0
84+
85+
val currentUsage = getCurrentConsumedTokens()
86+
val usedTokens = currentUsage.totalTokens
87+
88+
return if (usedTokens >= maxTokens) {
89+
0.0
90+
} else {
91+
1.0 - (usedTokens.toDouble() / maxTokens.toDouble())
92+
}
93+
}
94+
95+
/**
96+
* Calculate token usage ratio (0.0 to 1.0)
97+
* Returns the percentage of tokens already used
98+
*/
99+
fun calculateTokenUsageRatio(): Double {
100+
return 1.0 - calculateTokenAvailability()
101+
}
102+
103+
/**
104+
* Check if token usage is approaching the limit
105+
* @param threshold Warning threshold (default 0.8 = 80%)
106+
*/
107+
fun isApproachingTokenLimit(threshold: Double = 0.8): Boolean {
108+
return calculateTokenUsageRatio() >= threshold
109+
}
110+
111+
/**
112+
* Get remaining tokens for current session
113+
*/
114+
fun getRemainingTokens(): Long {
115+
val maxTokens = getUsedMaxToken()
116+
val currentUsage = getCurrentConsumedTokens()
117+
val remaining = maxTokens - currentUsage.totalTokens
118+
return maxOf(0L, remaining)
119+
}
120+
121+
fun dispose() {
122+
messageBusConnection?.disconnect()
123+
messageBusConnection = null
124+
}
125+
126+
companion object {
127+
fun getInstance(project: Project): TokenUsageService {
128+
return project.getService(TokenUsageService::class.java)
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)