opencode-models-usage-plugin 0.1.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md ADDED
@@ -0,0 +1,113 @@
1
+ # opencode-models-usage-plugin
2
+
3
+ **言語:** [English](./README.md) | [简体中文](./README.zh-CN.md) | 日本語 | [한국어](./README.ko.md)
4
+
5
+ **ついに、OpenCode のサイドバーで「AI モデルチームが今何をしているか」をリアルタイムで見られるようになりました。**
6
+
7
+ 複数モデルや多段 Agent、さらに OMO のサブセッションまで使って本格的に Coding していると、こんな不安を感じることはありませんか?
8
+
9
+ - 今どのモデルが出力中なのか?速度は速いのか遅いのか?
10
+ - すでにどれだけ tokens を消費したのか?サブ Agent が別のモデルを勝手に呼んでいないか?
11
+ - キャッシュのヒット状況はどうか?この coding plan はまだ持ちそうか?
12
+ - コストが急に跳ね上がらないか?
13
+
14
+ **もう、そうした不安に振り回される必要はありません。**
15
+
16
+ `opencode-models-usage-plugin` は、現在のセッションツリー全体におけるモデル使用状況を見える化します。何が起きているかを把握しながら、より安心して AI チームを動かせるようになります。
17
+
18
+ ![Models Usage sidebar screenshot](./src/assets/img.png)
19
+
20
+ ## 何が表示されるのか
21
+
22
+ OpenCode のサイドバーで、現在のセッションツリー(再帰的な子セッションを含む)に登場した各モデルの使用状況を **provider/model** 単位で集約表示します。
23
+
24
+ - **Live TPS**: ストリーミング出力中の tokens per second。アイドル時は `—` に戻ります
25
+ - **Context Tokens**: そのモデルの直近の返信に対応する token 総数
26
+ - **Session Tokens**: 現在のセッション内でそのモデルが累積消費した token 総数
27
+ - **Session Cached**: 現在のセッション内でそのモデルが累積でキャッシュヒットした token 数
28
+ - **Total Cost**: 現時点までの累積コスト(`spent $x.xxxx` 形式)
29
+
30
+ モデルが現在アクティブに出力しているときは、モデル名が**ハイライト表示**されるため、「今どのモデルが動いているか」を一目で把握できます。
31
+
32
+ ## サイドバー表示例
33
+
34
+ ```text
35
+ 📊 Models Usage
36
+ ● OpenAI/GPT-5.4 Fast
37
+ ■ TPS 23.1
38
+ ■ Context Tokens 12,345
39
+ ■ Session Tokens 84,163
40
+ ■ Session Cached 81,792
41
+ ■ spent $0.0000
42
+
43
+ ● Google/Gemini-2.5-Pro
44
+ ■ TPS —
45
+ ■ Context Tokens 8,942
46
+ ■ Session Tokens 45,672
47
+ ■ Session Cached 32,100
48
+ ■ spent $0.0123
49
+ ```
50
+
51
+ ## これが重要な理由
52
+
53
+ - **完全な透明性**: OMO サブ Agent や再帰呼び出しによる使用量も自動で集約され、見えないコストがなくなります
54
+ - **リアルタイム把握**: Live TPS、キャッシュヒット、累積コストをその場で確認できます
55
+ - **判断しやすい**: 遅いモデルやキャッシュ効率の良いモデルを素早く見分け、戦略を調整できます
56
+ - **安心して Coding**: 突然の高額請求や token 枯渇を気にしすぎず、開発そのものに集中できます
57
+
58
+ ## 動作要件
59
+
60
+ - OpenCode `>= 1.4.3`
61
+
62
+ ## インストール
63
+
64
+ ### 方法 1: グローバル TUI 設定を手動で更新する(初回におすすめ)
65
+
66
+ 以下のグローバル設定ファイルを開くか、新規作成します。
67
+
68
+ ```text
69
+ ~/.config/opencode/tui.json
70
+ ```
71
+
72
+ 次の内容を追加してください。
73
+
74
+ ```json
75
+ {
76
+ "$schema": "https://opencode.ai/tui.json",
77
+ "plugin": [
78
+ "opencode-models-usage-plugin/tui"
79
+ ],
80
+ "plugin_enabled": {
81
+ "session-model-usage": true
82
+ }
83
+ }
84
+ ```
85
+
86
+ 保存して OpenCode を再起動すると、npm 経由で自動インストールされます。
87
+
88
+ ### 方法 2: CLI でワンコマンドインストール
89
+
90
+ ```bash
91
+ opencode plugin opencode-models-usage-plugin --global
92
+ ```
93
+
94
+ このコマンドでプラグインのインストールとグローバル設定の更新を自動で行えます。
95
+
96
+ ## 技術メモ
97
+
98
+ - データは provider/model label 単位で厳密に集約されます
99
+ - OMO サブ Agent や再帰的な子セッションも完全にサポートします
100
+ - TPS は live-only で、ストリーミング中のみ更新され、アイドル時には `—` に戻ります
101
+ - Context Tokens は直近の返信に対応します
102
+ - Session Tokens と Session Cached は現在のセッションツリー全体での累積値です
103
+
104
+ ## パッケージ情報
105
+
106
+ - npm: `opencode-models-usage-plugin`
107
+ - 現在のバージョン: `0.1.0`
108
+
109
+ ## License
110
+
111
+ `Apache-2.0`
112
+
113
+ インストール後、ぜひすぐに試してみてください。サイドバーにモデルの状態が整然と並ぶのを見た瞬間、AI Coding がもっと落ち着いて、もっと扱いやすく感じられるはずです。
package/README.ko.md ADDED
@@ -0,0 +1,113 @@
1
+ # opencode-models-usage-plugin
2
+
3
+ **언어:** [English](./README.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) | 한국어
4
+
5
+ **이제 OpenCode 사이드바에서 “내 AI 모델 팀이 지금 무엇을 하고 있는지”를 실시간으로 볼 수 있습니다.**
6
+
7
+ 여러 모델과 다층 Agent, 그리고 OMO 하위 세션까지 함께 사용하며 Coding 할 때 이런 불안을 느낀 적이 있지 않나요?
8
+
9
+ - 지금 실제로 어느 모델이 출력 중인지? 속도는 빠른지 느린지?
10
+ - 이미 얼마나 많은 tokens 를 소비했는지? 하위 Agent 가 다른 모델을 몰래 호출하고 있지는 않은지?
11
+ - 캐시 적중은 얼마나 잘 되고 있는지? 지금 coding plan 이 얼마나 더 버틸 수 있는지?
12
+ - 비용이 갑자기 크게 늘어나지는 않을지?
13
+
14
+ **이제 그런 불안에서 훨씬 자유로워질 수 있습니다.**
15
+
16
+ `opencode-models-usage-plugin` 은 현재 세션 트리 전체에서 모델 사용 현황을 투명하게 보여줍니다. 무슨 일이 벌어지고 있는지 이해한 상태로 AI 팀을 더 자신 있게 운용할 수 있습니다.
17
+
18
+ ![Models Usage sidebar screenshot](./src/assets/img.png)
19
+
20
+ ## 무엇을 보여주나요?
21
+
22
+ OpenCode 사이드바에서 현재 세션 트리(재귀적인 하위 세션 포함)에 등장한 각 모델의 사용 현황을 **provider/model** 기준으로 집계하여 보여줍니다.
23
+
24
+ - **Live TPS**: 스트리밍 출력 중인 현재 tokens per second. idle 상태가 되면 `—` 로 돌아갑니다
25
+ - **Context Tokens**: 해당 모델의 가장 최근 응답에 해당하는 token 총량
26
+ - **Session Tokens**: 현재 세션에서 해당 모델이 누적 소비한 token 총량
27
+ - **Session Cached**: 현재 세션에서 해당 모델이 누적 캐시 적중한 token 총량
28
+ - **Total Cost**: 지금까지 발생한 누적 비용 (`spent $x.xxxx` 형식)
29
+
30
+ 어떤 모델이 현재 활발히 출력 중이면 그 모델 이름이 **하이라이트**되어, 지금 누가 일을 하고 있는지 바로 알 수 있습니다.
31
+
32
+ ## 사이드바 예시
33
+
34
+ ```text
35
+ 📊 Models Usage
36
+ ● OpenAI/GPT-5.4 Fast
37
+ ■ TPS 23.1
38
+ ■ Context Tokens 12,345
39
+ ■ Session Tokens 84,163
40
+ ■ Session Cached 81,792
41
+ ■ spent $0.0000
42
+
43
+ ● Google/Gemini-2.5-Pro
44
+ ■ TPS —
45
+ ■ Context Tokens 8,942
46
+ ■ Session Tokens 45,672
47
+ ■ Session Cached 32,100
48
+ ■ spent $0.0123
49
+ ```
50
+
51
+ ## 왜 중요한가요?
52
+
53
+ - **완전한 투명성**: OMO 하위 Agent 와 재귀 호출에서 생긴 사용량까지 자동으로 집계되어 숨겨진 비용이 없어집니다
54
+ - **실시간 통제력**: Live TPS, 캐시 적중, 누적 비용을 한눈에 확인할 수 있습니다
55
+ - **더 나은 판단**: 느린 모델과 캐시 효율이 좋은 모델을 빠르게 구분해 전략을 조정할 수 있습니다
56
+ - **덜 불안한 Coding**: 갑작스러운 비용 증가나 token 고갈을 덜 걱정하고 개발에 집중할 수 있습니다
57
+
58
+ ## 요구 사항
59
+
60
+ - OpenCode `>= 1.4.3`
61
+
62
+ ## 설치 방법
63
+
64
+ ### 방법 1: 전역 TUI 설정 파일을 수동으로 수정하기 (처음엔 이 방법 추천)
65
+
66
+ 다음 전역 설정 파일을 열거나 새로 만드세요.
67
+
68
+ ```text
69
+ ~/.config/opencode/tui.json
70
+ ```
71
+
72
+ 아래 내용을 추가하세요.
73
+
74
+ ```json
75
+ {
76
+ "$schema": "https://opencode.ai/tui.json",
77
+ "plugin": [
78
+ "opencode-models-usage-plugin/tui"
79
+ ],
80
+ "plugin_enabled": {
81
+ "session-model-usage": true
82
+ }
83
+ }
84
+ ```
85
+
86
+ 저장한 뒤 OpenCode 를 재시작하면 npm 을 통해 자동으로 설치됩니다.
87
+
88
+ ### 방법 2: CLI 한 줄로 설치하기
89
+
90
+ ```bash
91
+ opencode plugin opencode-models-usage-plugin --global
92
+ ```
93
+
94
+ 이 명령은 플러그인을 설치하고 전역 설정도 자동으로 갱신합니다.
95
+
96
+ ## 기술 메모
97
+
98
+ - 데이터는 provider/model label 기준으로 엄격하게 집계됩니다
99
+ - OMO 하위 Agent 와 재귀적인 하위 세션을 완전히 지원합니다
100
+ - TPS 는 live-only 이며, 스트리밍 중에만 갱신되고 idle 상태가 되면 `—` 로 돌아갑니다
101
+ - Context Tokens 는 가장 최근 응답의 token 총량을 나타냅니다
102
+ - Session Tokens 와 Session Cached 는 현재 세션 트리 전체의 누적값입니다
103
+
104
+ ## 패키지 정보
105
+
106
+ - npm: `opencode-models-usage-plugin`
107
+ - 현재 버전: `0.1.0`
108
+
109
+ ## License
110
+
111
+ `Apache-2.0`
112
+
113
+ 설치 후 바로 한 번 써 보세요. 사이드바에 모든 모델의 상태가 선명하게 정리되어 보이는 순간, AI Coding 이 훨씬 더 차분하고 통제 가능하게 느껴질 것입니다.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # opencode-models-usage-plugin
2
+
3
+ **Languages:** English | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md)
4
+
5
+ **Finally, you can see exactly what your AI model team is doing — right in the OpenCode sidebar.**
6
+
7
+ When you're coding with multiple models and multi-layer agents, including OMO sub-sessions, do you ever worry about things like:
8
+
9
+ - Which model is actively generating right now? Is it fast or slow?
10
+ - How many tokens have already been consumed? Are sub-agents quietly calling other models?
11
+ - How effective is cache reuse? How long can this coding plan keep going?
12
+ - Is the bill about to spike?
13
+
14
+ **Those anxieties can now be a thing of the past.**
15
+
16
+ `opencode-models-usage-plugin` gives you clear visibility into model usage across the entire session tree. You can see what is happening, stay in control, and drive your AI team with much more confidence.
17
+
18
+ ![Models Usage sidebar screenshot](./src/assets/img.png)
19
+
20
+ ## What It Displays
21
+
22
+ In the OpenCode sidebar, the plugin aggregates real-time usage for every model used in the current session tree, including all recursive sub-sessions, grouped by **provider/model**:
23
+
24
+ - **Live TPS**: shows current tokens per second while a response is streaming, and automatically returns to `—` when idle
25
+ - **Context Tokens**: the token total for that model's most recent reply, used as the current context footprint
26
+ - **Session Tokens**: the cumulative token total consumed by that model in the current session
27
+ - **Session Cached**: the cumulative cache-hit tokens for that model in the current session, so you can see how much cache reuse is helping
28
+ - **Total Cost**: the amount spent so far, shown as `spent $x.xxxx`
29
+
30
+ When a model is actively streaming, its name is **highlighted** so you can instantly see who is doing the work.
31
+
32
+ ## Real Sidebar Example
33
+
34
+ ```text
35
+ 📊 Models Usage
36
+ ● OpenAI/GPT-5.4 Fast
37
+ ■ TPS 23.1
38
+ ■ Context Tokens 12,345
39
+ ■ Session Tokens 84,163
40
+ ■ Session Cached 81,792
41
+ ■ spent $0.0000
42
+
43
+ ● Google/Gemini-2.5-Pro
44
+ ■ TPS —
45
+ ■ Context Tokens 8,942
46
+ ■ Session Tokens 45,672
47
+ ■ Session Cached 32,100
48
+ ■ spent $0.0123
49
+
50
+ ● MiniMax/MiniMax-Text-01 (sub-session)
51
+ ■ TPS 41.7
52
+ ■ Context Tokens ...
53
+ ■ Session Tokens ...
54
+ ■ Session Cached ...
55
+ ■ spent ...
56
+ ```
57
+
58
+ ## Why It Matters
59
+
60
+ - **Total Transparency**: usage from OMO sub-agents and recursive calls is automatically aggregated, so there are no more hidden costs
61
+ - **Real-Time Control**: see live TPS, cache hits, and cumulative costs at a glance
62
+ - **Smarter Decisions**: quickly spot slow models or strong cache performers and adjust your strategy on the fly
63
+ - **Lower-Stress Coding**: stop worrying about surprise bills or token drain, and focus on building
64
+
65
+ Whether you are heavily using model routing, complex agent orchestration, or simply want better visibility into token usage, this plugin can make the experience far more transparent and reassuring.
66
+
67
+ ## Requirements
68
+
69
+ - OpenCode `>= 1.4.3`
70
+
71
+ ## Installation
72
+
73
+ ### Method 1: Manually update the global TUI config (recommended for first-time setup)
74
+
75
+ Open or create the global config file:
76
+
77
+ ```text
78
+ ~/.config/opencode/tui.json
79
+ ```
80
+
81
+ Add the following:
82
+
83
+ ```json
84
+ {
85
+ "$schema": "https://opencode.ai/tui.json",
86
+ "plugin": [
87
+ "opencode-models-usage-plugin/tui"
88
+ ],
89
+ "plugin_enabled": {
90
+ "session-model-usage": true
91
+ }
92
+ }
93
+ ```
94
+
95
+ Save the file and restart OpenCode. The plugin will be installed automatically via npm.
96
+
97
+ ### Method 2: One-click install via CLI
98
+
99
+ ```bash
100
+ opencode plugin opencode-models-usage-plugin --global
101
+ ```
102
+
103
+ This command installs the plugin and updates your global config automatically.
104
+
105
+ ## Technical Notes
106
+
107
+ - Data is grouped strictly by provider/model label
108
+ - Fully supports OMO sub-agents and all recursive sub-sessions, including Gemini, MiniMax, and others
109
+ - TPS is strictly live-only: it updates only during streaming and returns to `—` when idle
110
+ - Context Tokens reflect the token total of the most recent reply
111
+ - Session Tokens and Session Cached reflect cumulative values across the current session tree
112
+
113
+ ## Package Info
114
+
115
+ - npm: `opencode-models-usage-plugin`
116
+ - current version: `0.1.0`
117
+
118
+ ## License
119
+
120
+ `Apache-2.0`
121
+
122
+ Try it right after installing — the first time you see all your model activity laid out clearly in the sidebar, you may realize that AI coding can finally feel much calmer and far more under control.
123
+
124
+ Issues, PRs, and feedback are always welcome.
125
+
126
+ Let’s make the OpenCode model ecosystem more transparent and more usable together. 🚀
@@ -0,0 +1,127 @@
1
+ # opencode-models-usage-plugin
2
+
3
+ **语言:** [English](./README.md) | 简体中文 | [日本語](./README.ja.md) | [한국어](./README.ko.md)
4
+
5
+ **终于,你可以在 OpenCode 侧边栏里实时看到「你的 AI 模型团队」到底在干什么了。**
6
+
7
+ 当你同时使用多个模型、多层 Agent(包括 OMO 子会话)全力 Coding 时,是否经常产生这些焦虑:
8
+
9
+ - 这个模型现在到底在不在输出?速度快不快?
10
+ - 已经悄悄消耗了多少 tokens?子 Agent 有没有偷偷调用其他模型?
11
+ - 缓存命中情况怎么样?这次 coding plan 还能撑多久?
12
+ - 账单会不会突然暴涨?
13
+
14
+ **现在,这些担心可以彻底告别了。**
15
+
16
+ `opencode-models-usage-plugin` 让你对整个会话树中的所有模型使用情况**一目了然**、**心中有数**。你可以更放心地驱动 AI 团队为你工作,而不必再畏手畏脚、时刻提心吊胆。
17
+
18
+ ![Models Usage sidebar screenshot](./src/assets/img.png)
19
+
20
+
21
+ ## 它能显示什么?
22
+
23
+ 在 OpenCode 的侧边栏中,插件会按 **provider/model** 聚合展示当前会话树(含所有递归子会话)中每个模型的实时数据:
24
+
25
+ - **实时 TPS**:正在流式输出时显示当前 tokens per second,输出结束或空闲时自动变为 `—`
26
+ - **Context Tokens**:该模型最近一次回复对应的 token 总数,用来表示这一轮上下文占用
27
+ - **Session Tokens**:当前会话中该模型累计消耗的 token 总数
28
+ - **Session Cached**:当前会话中该模型累计缓存命中的 token 数,能直观看到缓存节省了多少
29
+ - **Total Cost**:当前已产生的费用(显示为 `spent $x.xxxx`)
30
+
31
+ 当某个模型正在活跃输出时,它的名称会**高亮显示**,让你一眼就知道「谁在干活」。
32
+
33
+ ## 侧边栏实际显示示例
34
+
35
+ ```text
36
+ 📊 Models Usage
37
+ ● OpenAI/GPT-5.4 Fast
38
+ ■ TPS 23.1
39
+ ■ Context Tokens 12,345
40
+ ■ Session Tokens 84,163
41
+ ■ Session Cached 81,792
42
+ ■ spent $0.0000
43
+
44
+ ● Google/Gemini-2.5-Pro
45
+ ■ TPS —
46
+ ■ Context Tokens 8,942
47
+ ■ Session Tokens 45,672
48
+ ■ Session Cached 32,100
49
+ ■ spent $0.0123
50
+
51
+ ● MiniMax/MiniMax-Text-01(子会话)
52
+ ■ TPS 41.7
53
+ ■ Context Tokens ...
54
+ ■ Session Tokens ...
55
+ ■ Session Cached ...
56
+ ■ spent ...
57
+ ```
58
+
59
+ ## 核心价值
60
+
61
+ - **完全透明**:OMO 子 Agent、递归调用产生的消耗都会自动聚合,不再有“黑箱烧钱”
62
+ - **实时掌控**:随时看到 TPS、缓存命中和累计成本,对预算和性能心里有数
63
+ - **高效决策**:快速判断哪个模型响应慢、哪个缓存效果好,随时优化策略
64
+ - **低焦虑 Coding**:告别对账单暴涨、token 耗尽的恐惧,把注意力放回创作本身
65
+
66
+ 无论你是重度使用多模型路由、复杂 Agent 编排,还是只是想对 token 消耗更有掌控感,这个插件都能显著提升你的使用体验和安心感。
67
+
68
+ ## 运行要求
69
+
70
+ - OpenCode `>= 1.4.3`
71
+
72
+ ## 安装方式
73
+
74
+ ### 方式一:手动修改全局 TUI 配置(推荐第一次使用)
75
+
76
+ 打开或新建全局配置文件:
77
+
78
+ ```text
79
+ ~/.config/opencode/tui.json
80
+ ```
81
+
82
+ 添加以下内容:
83
+
84
+ ```json
85
+ {
86
+ "$schema": "https://opencode.ai/tui.json",
87
+ "plugin": [
88
+ "opencode-models-usage-plugin/tui"
89
+ ],
90
+ "plugin_enabled": {
91
+ "session-model-usage": true
92
+ }
93
+ }
94
+ ```
95
+
96
+ 保存后重启 OpenCode,插件会自动通过 npm 安装。
97
+
98
+ ### 方式二:CLI 一键安装(最便捷)
99
+
100
+ ```bash
101
+ opencode plugin opencode-models-usage-plugin --global
102
+ ```
103
+
104
+ 这条命令会自动安装插件,并更新全局配置。
105
+
106
+ ## 技术说明
107
+
108
+ - 数据按 provider/model label 严格聚合
109
+ - 完整支持 OMO 子 Agent 及所有递归子会话,Gemini、MiniMax 等子模型调用也会被统计
110
+ - TPS 为严格实时:仅在流式输出过程中动态更新,空闲状态自动恢复为 `—`
111
+ - Context Tokens 反映最近一次回复对应的 token 总数
112
+ - Session Tokens 与 Session Cached 反映当前整个会话树中的累计值
113
+
114
+ ## 包信息
115
+
116
+ - npm:`opencode-models-usage-plugin`
117
+ - 当前版本:`0.1.0`
118
+
119
+ ## License
120
+
121
+ `Apache-2.0`
122
+
123
+ 安装后立刻试试看——当你第一次在侧边栏看到所有模型数据清晰、安静地排列在那里时,你会真切感受到:用 AI Coding,从此可以更放松、更可控。
124
+
125
+ 欢迎提交 Issue、PR,或分享你的使用感受。
126
+
127
+ 让我们一起让 OpenCode 的模型世界更加透明、更好用。🚀
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "opencode-models-usage-plugin",
3
- "version": "0.1.0",
3
+ "version": "1.0.1",
4
4
  "description": "Show per-model usage, live TPS, tokens, and cost in the OpenCode sidebar.",
5
- "license": "UNLICENSED",
5
+ "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  "./tui": {
Binary file
@@ -32,7 +32,9 @@ test("assistant message contributes all token counters and cost", () => {
32
32
  models: [
33
33
  {
34
34
  label: "openai/gpt-4.1",
35
- totalTokens: 28,
35
+ contextTokens: 28,
36
+ sessionTokens: 28,
37
+ sessionCachedTokens: 3,
36
38
  cost: 0.05,
37
39
  tps: 0,
38
40
  isLiveTps: false,
@@ -41,7 +43,7 @@ test("assistant message contributes all token counters and cost", () => {
41
43
  })
42
44
  })
43
45
 
44
- test("repeated model is listed once while tokens keep accumulating", () => {
46
+ test("repeated model uses the latest output-bearing message for tokens", () => {
45
47
  const messages: SessionMessageLike[] = [
46
48
  {
47
49
  role: "assistant",
@@ -62,6 +64,10 @@ test("repeated model is listed once while tokens keep accumulating", () => {
62
64
  tokens: {
63
65
  input: 4,
64
66
  output: 6,
67
+ cache: {
68
+ read: 7,
69
+ write: 1,
70
+ },
65
71
  },
66
72
  time: { created: 4000, completed: 5000 },
67
73
  },
@@ -71,10 +77,12 @@ test("repeated model is listed once while tokens keep accumulating", () => {
71
77
  models: [
72
78
  {
73
79
  label: "openai/gpt-4.1",
74
- totalTokens: 25,
80
+ contextTokens: 18,
81
+ sessionTokens: 33,
82
+ sessionCachedTokens: 7,
75
83
  cost: 0.03,
76
84
  tps: 0,
77
- isLiveTps: false, // (5 + 6) tokens / (2 + 1) seconds = 11 / 3
85
+ isLiveTps: false,
78
86
  },
79
87
  ],
80
88
  })
@@ -106,8 +114,12 @@ test("multiple models preserve first-seen order", () => {
106
114
  "anthropic/claude-sonnet-4",
107
115
  "openai/gpt-4.1",
108
116
  ])
109
- assert.equal(usage.models[0].totalTokens, 3)
110
- assert.equal(usage.models[1].totalTokens, 7)
117
+ assert.equal(usage.models[0].contextTokens, 3)
118
+ assert.equal(usage.models[0].sessionTokens, 3)
119
+ assert.equal(usage.models[0].sessionCachedTokens, 0)
120
+ assert.equal(usage.models[1].contextTokens, 7)
121
+ assert.equal(usage.models[1].sessionTokens, 7)
122
+ assert.equal(usage.models[1].sessionCachedTokens, 0)
111
123
  })
112
124
 
113
125
  test("non-assistant messages are ignored", () => {
@@ -138,7 +150,9 @@ test("non-assistant messages are ignored", () => {
138
150
  models: [
139
151
  {
140
152
  label: "openai/gpt-4.1",
141
- totalTokens: 5,
153
+ contextTokens: 5,
154
+ sessionTokens: 5,
155
+ sessionCachedTokens: 0,
142
156
  cost: 0.1,
143
157
  tps: 0,
144
158
  isLiveTps: false,
@@ -180,7 +194,9 @@ test("provider catalog names are used when available", () => {
180
194
  models: [
181
195
  {
182
196
  label: "OpenAI/GPT-4.1",
183
- totalTokens: 5,
197
+ contextTokens: 5,
198
+ sessionTokens: 5,
199
+ sessionCachedTokens: 0,
184
200
  cost: 0,
185
201
  tps: 0,
186
202
  isLiveTps: false,
@@ -222,7 +238,9 @@ test("provider catalog also works when providers are stored as an array", () =>
222
238
  models: [
223
239
  {
224
240
  label: "Anthropic/Claude Sonnet 4",
225
- totalTokens: 10,
241
+ contextTokens: 10,
242
+ sessionTokens: 10,
243
+ sessionCachedTokens: 0,
226
244
  cost: 0,
227
245
  tps: 0,
228
246
  isLiveTps: false,
@@ -243,65 +261,50 @@ test("token helper tolerates missing counters", () => {
243
261
  )
244
262
  })
245
263
 
246
- test("tps eligibility rules are applied correctly", () => {
264
+ test("latest output-bearing assistant message determines displayed tokens and cache", () => {
247
265
  const usage = getSessionUsage([
248
- // valid
249
266
  {
250
267
  role: "assistant",
251
268
  providerID: "openai",
252
269
  modelID: "gpt-4",
253
- tokens: { output: 10 },
254
- time: { created: 1000, completed: 2000 },
255
- },
256
- // ignored: summary = true
257
- {
258
- role: "assistant",
259
- providerID: "openai",
260
- modelID: "gpt-4",
261
- summary: true,
262
- tokens: { output: 100 },
263
- time: { created: 1000, completed: 2000 },
264
- },
265
- // ignored: no completed time
266
- {
267
- role: "assistant",
268
- providerID: "openai",
269
- modelID: "gpt-4",
270
- tokens: { output: 100 },
271
- time: { created: 1000 },
272
- },
273
- // ignored: duration <= 0
274
- {
275
- role: "assistant",
276
- providerID: "openai",
277
- modelID: "gpt-4",
278
- tokens: { output: 100 },
279
- time: { created: 1000, completed: 1000 },
280
- },
281
- // ignored: generated tokens <= 0
282
- {
283
- role: "assistant",
284
- providerID: "openai",
285
- modelID: "gpt-4",
286
- tokens: { input: 100, output: 0 },
270
+ tokens: {
271
+ input: 5,
272
+ output: 10,
273
+ cache: {
274
+ read: 20,
275
+ write: 2,
276
+ },
277
+ },
287
278
  time: { created: 1000, completed: 2000 },
288
279
  },
289
- // ignored: finish === "tool-calls"
290
280
  {
291
281
  role: "assistant",
292
282
  providerID: "openai",
293
283
  modelID: "gpt-4",
294
- finish: "tool-calls",
295
- tokens: { output: 100 },
296
- time: { created: 1000, completed: 2000 },
284
+ tokens: {
285
+ input: 100,
286
+ output: 0,
287
+ cache: {
288
+ read: 999,
289
+ write: 999,
290
+ },
291
+ },
292
+ time: { created: 3000, completed: 4000 },
297
293
  },
298
- // valid: output + reasoning
299
294
  {
300
295
  role: "assistant",
301
296
  providerID: "openai",
302
297
  modelID: "gpt-4",
303
- tokens: { output: 5, reasoning: 5 },
304
- time: { created: 2000, completed: 3000 },
298
+ tokens: {
299
+ input: 11,
300
+ output: 7,
301
+ reasoning: 3,
302
+ cache: {
303
+ read: 30,
304
+ write: 4,
305
+ },
306
+ },
307
+ time: { created: 5000, completed: 6000 },
305
308
  },
306
309
  ])
307
310
 
@@ -309,18 +312,13 @@ test("tps eligibility rules are applied correctly", () => {
309
312
  models: [
310
313
  {
311
314
  label: "openai/gpt-4",
312
- totalTokens: 520, // 10 + 100 + 100 + 100 + 100(input) + 100 + 10 = 520 actually wait
313
- // Tokens: 10 + 100 + 100 + 100 + 100 + 100 + 10 = 520
314
- // Cost: 0
315
- // TPS: 20 tokens / 2 seconds = 10
315
+ contextTokens: 55,
316
+ sessionTokens: 2190,
317
+ sessionCachedTokens: 1049,
316
318
  cost: 0,
317
319
  tps: 0,
318
320
  isLiveTps: false,
319
321
  },
320
322
  ],
321
323
  })
322
-
323
- // Update token expectation to match manual addition
324
- const total = 10 + 100 + 100 + 100 + 100 + 100 + 10;
325
- assert.equal(usage.models[0].totalTokens, total)
326
324
  })
@@ -26,7 +26,9 @@ export interface SessionMessageLike {
26
26
  export interface ModelUsage {
27
27
  label: string
28
28
  tps: number
29
- totalTokens: number
29
+ contextTokens: number
30
+ sessionTokens: number
31
+ sessionCachedTokens: number
30
32
  cost: number
31
33
  isLiveTps?: boolean
32
34
  }
@@ -105,6 +107,20 @@ export interface LiveTpsState {
105
107
  tpsDurationMs: number
106
108
  }
107
109
 
110
+ const getMessageTimestamp = (message: SessionMessageLike, index: number): number => {
111
+ const completed = toNumber(message.time?.completed)
112
+ if (completed > 0) {
113
+ return completed
114
+ }
115
+
116
+ const created = toNumber(message.time?.created)
117
+ if (created > 0) {
118
+ return created
119
+ }
120
+
121
+ return index
122
+ }
123
+
108
124
  export const getSessionUsage = (
109
125
  messages: ReadonlyArray<SessionMessageLike>,
110
126
  providers?: unknown,
@@ -112,12 +128,15 @@ export const getSessionUsage = (
112
128
  ): SessionUsage => {
113
129
  const modelStats = new Map<string, {
114
130
  label: string
115
- totalTokens: number
131
+ contextTokens: number
132
+ sessionTokens: number
133
+ sessionCachedTokens: number
116
134
  cost: number
135
+ latestTimestamp: number
117
136
  }>()
118
137
  const modelsUsed: string[] = []
119
138
 
120
- for (const message of messages) {
139
+ for (const [index, message] of messages.entries()) {
121
140
  if (message.role !== "assistant") {
122
141
  continue
123
142
  }
@@ -134,15 +153,32 @@ export const getSessionUsage = (
134
153
  if (!modelStats.has(modelKey)) {
135
154
  modelStats.set(modelKey, {
136
155
  label: resolveModelLabel(modelID, providerID, providers),
137
- totalTokens: 0,
156
+ contextTokens: 0,
157
+ sessionTokens: 0,
158
+ sessionCachedTokens: 0,
138
159
  cost: 0,
160
+ latestTimestamp: Number.NEGATIVE_INFINITY,
139
161
  })
140
162
  modelsUsed.push(modelKey)
141
163
  }
142
164
 
143
165
  const stats = modelStats.get(modelKey)!
144
- stats.totalTokens += getTokenTotal(message.tokens)
145
166
  stats.cost += toNumber(message.cost)
167
+ stats.sessionTokens += getTokenTotal(message.tokens)
168
+ stats.sessionCachedTokens += toNumber(message.tokens?.cache?.read)
169
+
170
+ const outputTokens = toNumber(message.tokens?.output)
171
+ if (outputTokens <= 0) {
172
+ continue
173
+ }
174
+
175
+ const timestamp = getMessageTimestamp(message, index)
176
+ if (timestamp < stats.latestTimestamp) {
177
+ continue
178
+ }
179
+
180
+ stats.latestTimestamp = timestamp
181
+ stats.contextTokens = getTokenTotal(message.tokens)
146
182
  }
147
183
 
148
184
  // Inject live TPS
@@ -154,8 +190,11 @@ export const getSessionUsage = (
154
190
  if (!modelStats.has(liveTpsKey)) {
155
191
  modelStats.set(liveTpsKey, {
156
192
  label: resolveModelLabel(modelID, providerID, providers),
157
- totalTokens: 0,
193
+ contextTokens: 0,
194
+ sessionTokens: 0,
195
+ sessionCachedTokens: 0,
158
196
  cost: 0,
197
+ latestTimestamp: Number.NEGATIVE_INFINITY,
159
198
  })
160
199
  modelsUsed.push(liveTpsKey)
161
200
  }
@@ -174,7 +213,9 @@ export const getSessionUsage = (
174
213
 
175
214
  return {
176
215
  label: stats.label,
177
- totalTokens: stats.totalTokens,
216
+ contextTokens: stats.contextTokens,
217
+ sessionTokens: stats.sessionTokens,
218
+ sessionCachedTokens: stats.sessionCachedTokens,
178
219
  cost: stats.cost,
179
220
  tps,
180
221
  isLiveTps,
package/src/tui.tsx CHANGED
@@ -1,11 +1,12 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
 
3
3
  import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
4
- import { For, createMemo, createSignal, onMount, onCleanup } from "solid-js"
4
+ import type { Accessor } from "solid-js"
5
+ import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
5
6
 
6
7
  import { getSessionUsage, type SessionMessageLike, type LiveTpsState } from "./session-usage"
7
8
 
8
- const pluginID = "local:session-model-usage"
9
+ const pluginID = "session-model-usage"
9
10
 
10
11
  const isRecord = (value: unknown): value is Record<string, unknown> =>
11
12
  typeof value === "object" && value !== null
@@ -39,10 +40,11 @@ const mapMessages = (messages: readonly unknown[]): SessionMessageLike[] => {
39
40
  return messages.map((message) => mapMessage(message))
40
41
  }
41
42
 
42
- const useSessionMessages = (api: TuiPluginApi, rootSessionID: string) => {
43
+ const useSessionMessages = (api: TuiPluginApi, rootSessionID: Accessor<string>) => {
44
+ const [rootMessages, setRootMessages] = createSignal<SessionMessageLike[]>([])
43
45
  const [extraMessages, setExtraMessages] = createSignal<SessionMessageLike[]>([])
44
46
  const [trackedSessionIDs, setTrackedSessionIDs] = createSignal<ReadonlySet<string>>(
45
- new Set([rootSessionID]),
47
+ new Set([rootSessionID()]),
46
48
  )
47
49
 
48
50
  const [liveInfo, setLiveInfo] = createSignal<{
@@ -53,6 +55,17 @@ const useSessionMessages = (api: TuiPluginApi, rootSessionID: string) => {
53
55
  chars: number;
54
56
  } | null>(null)
55
57
 
58
+ let reloadVersion = 0
59
+
60
+ const fetchRootMessages = async (sessionID: string): Promise<SessionMessageLike[]> => {
61
+ const result = await api.client.session.messages({ sessionID })
62
+ if (result.error || !result.data) {
63
+ return []
64
+ }
65
+
66
+ return mapMessages(result.data.map((message) => message.info))
67
+ }
68
+
56
69
  const fetchChildrenMessages = async (
57
70
  sessionID: string,
58
71
  visited = new Set<string>(),
@@ -70,7 +83,10 @@ const useSessionMessages = (api: TuiPluginApi, rootSessionID: string) => {
70
83
  let msgs: SessionMessageLike[] = []
71
84
  for (const child of res.data) {
72
85
  const childID = child.id
73
- visited.add(childID)
86
+ if (visited.has(childID)) {
87
+ continue
88
+ }
89
+
74
90
  const mRes = await api.client.session.messages({ sessionID: childID })
75
91
  if (!mRes.error && mRes.data) {
76
92
  msgs.push(...mapMessages(mRes.data.map((message) => message.info)))
@@ -81,19 +97,37 @@ const useSessionMessages = (api: TuiPluginApi, rootSessionID: string) => {
81
97
  return { messages: msgs, sessionIDs: visited }
82
98
  }
83
99
 
84
- const reloadExtraMessages = async () => {
85
- const result = await fetchChildrenMessages(rootSessionID)
100
+ const reloadExtraMessages = async (sessionID: string) => {
101
+ const version = ++reloadVersion
102
+ const [root, result] = await Promise.all([
103
+ fetchRootMessages(sessionID),
104
+ fetchChildrenMessages(sessionID),
105
+ ])
106
+
107
+ if (version !== reloadVersion) {
108
+ return
109
+ }
110
+
111
+ setRootMessages(root)
86
112
  setExtraMessages(result.messages)
87
113
  setTrackedSessionIDs(new Set(result.sessionIDs))
88
114
  }
89
115
 
90
- onMount(() => {
91
- reloadExtraMessages()
116
+ createEffect(() => {
117
+ const sessionID = rootSessionID()
118
+ setRootMessages([])
119
+ setExtraMessages([])
120
+ setTrackedSessionIDs(new Set([sessionID]))
121
+ setLiveInfo(null)
122
+ void reloadExtraMessages(sessionID)
123
+ })
92
124
 
125
+ onMount(() => {
93
126
  const unsubs = [
94
127
  api.event.on("message.part.delta", (ev) => {
95
128
  const { sessionID, messageID, field, delta } = ev.properties
96
129
  if (!sessionID || !messageID || typeof delta !== "string") return
130
+ if (!trackedSessionIDs().has(sessionID)) return
97
131
  if (field !== "text" && field !== "reasoning") return
98
132
 
99
133
  setLiveInfo(prev => {
@@ -102,9 +136,13 @@ const useSessionMessages = (api: TuiPluginApi, rootSessionID: string) => {
102
136
  }
103
137
 
104
138
  // Try to find the message
105
- const rootMsgs = mapMessages(api.state.session.messages(rootSessionID))
139
+ const rootMsgs = mapMessages(api.state.session.messages(rootSessionID()))
140
+ const historicalRootMessages = rootMessages()
106
141
  const extra = extraMessages()
107
142
  let found = rootMsgs.find(m => m.id === messageID)
143
+ if (!found) {
144
+ found = historicalRootMessages.find(m => m.id === messageID)
145
+ }
108
146
  if (!found) {
109
147
  found = extra.find(m => m.id === messageID)
110
148
  }
@@ -127,8 +165,12 @@ const useSessionMessages = (api: TuiPluginApi, rootSessionID: string) => {
127
165
  }
128
166
  })
129
167
  }),
130
- api.event.on("message.updated", () => reloadExtraMessages()),
131
- api.event.on("session.status", () => reloadExtraMessages()),
168
+ api.event.on("message.updated", () => {
169
+ void reloadExtraMessages(rootSessionID())
170
+ }),
171
+ api.event.on("session.status", () => {
172
+ void reloadExtraMessages(rootSessionID())
173
+ }),
132
174
  api.event.on("session.idle", (ev) => {
133
175
  if (!trackedSessionIDs().has(ev.properties.sessionID)) {
134
176
  return
@@ -139,7 +181,10 @@ const useSessionMessages = (api: TuiPluginApi, rootSessionID: string) => {
139
181
  ]
140
182
 
141
183
  onCleanup(() => {
142
- unsubs.forEach(u => u())
184
+ reloadVersion += 1
185
+ unsubs.forEach((unsubscribe) => {
186
+ unsubscribe()
187
+ })
143
188
  })
144
189
  })
145
190
 
@@ -167,36 +212,53 @@ const useSessionMessages = (api: TuiPluginApi, rootSessionID: string) => {
167
212
  }
168
213
  })
169
214
 
170
- return { extraMessages, liveTps }
215
+ const usageMessages = createMemo(() => {
216
+ return [...rootMessages(), ...extraMessages()]
217
+ })
218
+
219
+ return { usageMessages, liveTps }
171
220
  }
172
221
 
173
222
  const SessionUsagePanel = (props: { api: TuiPluginApi; sessionID: string }) => {
174
- const { extraMessages, liveTps } = useSessionMessages(props.api, props.sessionID)
175
-
176
- const usage = createMemo(() => {
177
- const rootMsgs = mapMessages(props.api.state.session.messages(props.sessionID))
178
- const allMsgs = [...rootMsgs, ...extraMessages()]
179
- return getSessionUsage(allMsgs, props.api.state.provider, liveTps())
223
+ const activeSessionID = createMemo<string>(() => {
224
+ const route = props.api.route.current
225
+ if (route.name === "session" && typeof route.params?.sessionID === "string") {
226
+ return route.params.sessionID
227
+ }
228
+
229
+ return props.sessionID
180
230
  })
181
231
 
232
+ const { usageMessages, liveTps } = useSessionMessages(props.api, activeSessionID)
233
+
234
+ const usage = createMemo(() => getSessionUsage(usageMessages(), props.api.state.provider, liveTps()))
235
+
182
236
  return (
183
237
  <box flexDirection="column">
184
- <text>📊 Models Usages</text>
238
+ <text>📊 Models Usage</text>
185
239
  {usage().models.length === 0 ? (
186
240
  <text>None yet</text>
187
241
  ) : (
188
242
  <For each={usage().models}>
189
243
  {(model) => (
190
244
  <box flexDirection="column">
191
- <text>● {model.label}</text>
245
+ <text fg={model.isLiveTps ? "yellow" : undefined}>● {model.label}</text>
192
246
  <box flexDirection="column" paddingLeft={2}>
193
247
  <box flexDirection="row">
194
248
  <text>■ TPS </text>
195
249
  <text fg="gray">{model.isLiveTps && model.tps > 0 ? model.tps.toFixed(1) : "—"}</text>
196
250
  </box>
197
251
  <box flexDirection="row">
198
- <text>■ Total Tokens </text>
199
- <text fg="gray">{model.totalTokens.toLocaleString("en-US")}</text>
252
+ <text>■ Context Tokens </text>
253
+ <text fg="gray">{model.contextTokens.toLocaleString("en-US")}</text>
254
+ </box>
255
+ <box flexDirection="row">
256
+ <text>■ Session Tokens </text>
257
+ <text fg="gray">{model.sessionTokens.toLocaleString("en-US")}</text>
258
+ </box>
259
+ <box flexDirection="row">
260
+ <text>■ Session Cached </text>
261
+ <text fg="gray">{model.sessionCachedTokens.toLocaleString("en-US")}</text>
200
262
  </box>
201
263
  <box flexDirection="row">
202
264
  <text>■ spent </text>
@@ -216,7 +278,11 @@ const tui: TuiPlugin = async (api) => {
216
278
  order: 150,
217
279
  slots: {
218
280
  sidebar_content(_ctx, props) {
219
- return <SessionUsagePanel api={api} sessionID={props.session_id} />
281
+ return (
282
+ <Show when={props.session_id} keyed>
283
+ {(sessionID: string) => <SessionUsagePanel api={api} sessionID={sessionID} />}
284
+ </Show>
285
+ )
220
286
  },
221
287
  },
222
288
  })