opencode-usage-insights 0.0.0-stage โ 0.1.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/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/core.d.ts +259 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +1227 -0
- package/dist/index.js.map +16 -0
- package/dist/line.d.ts +71 -0
- package/dist/panel-build.d.ts +46 -0
- package/dist/panel-data.d.ts +216 -0
- package/dist/panel.d.ts +39 -0
- package/dist/recorder.d.ts +69 -0
- package/dist/sidebar.d.ts +25 -0
- package/dist/stats/aggregates.d.ts +283 -0
- package/dist/stats/db.d.ts +99 -0
- package/dist/stats/maintenance.d.ts +49 -0
- package/dist/stats/paths.d.ts +35 -0
- package/dist/stats/queries.d.ts +239 -0
- package/dist/text.d.ts +25 -0
- package/dist/tui.d.ts +53 -0
- package/dist/tui.js +3515 -0
- package/dist/tui.js.map +22 -0
- package/package.json +84 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hugo Batista
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,324 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-usage-insights ๐
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/hugobatista/opencode-usage-insights/releases)
|
|
4
|
+
[](https://github.com/hugobatista/opencode-usage-insights/actions/workflows/test.yml)
|
|
5
|
+
[](https://github.com/hugobatista/opencode-usage-insights/actions/workflows/lint.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/opencode-usage-insights)
|
|
7
|
+
|
|
8
|
+
OpenCode plugin for one question the host cannot answer: **which provider is
|
|
9
|
+
actually serving each model** โ and what that costs. It shows a telemetry line
|
|
10
|
+
above the composer after every finished turn and records every assistant step
|
|
11
|
+
in a local SQLite file, read back through an interactive `/usage` panel.
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
Everything stays local: one SQLite file under the OpenCode state directory,
|
|
16
|
+
written by the server-side recorder and read by the TUI.
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
- **The line** (`session.composer.top`): end time, the gap since the previous
|
|
21
|
+
prompt, the provider and model that ran the turn, turn and session token
|
|
22
|
+
totals, and warnings when the prompt cache or the provider identity changed.
|
|
23
|
+
It belongs to finished work, so it stays hidden while a turn is running.
|
|
24
|
+
- **The `/usage` panel**: nine keyboard-driven views โ `overview`, `models`,
|
|
25
|
+
`providers`, `cache`, `periods`, `latency`, `sessions`, `turns`,
|
|
26
|
+
`db` โ over five ranges (`session`, `today`, `7d`, `30d`, `all`), with
|
|
27
|
+
drill-downs, sorting, filters and state remembered between openings.
|
|
28
|
+
`/usage models 7d`, `/usage providers 30d`, `/usage turns 7d`.
|
|
29
|
+
- **The session sidebar**: the session's own totals and its last turns, in the
|
|
30
|
+
same buckets as the line. Three lines per turn โ when and through which
|
|
31
|
+
provider, the prompt volumes, then `hit ยท tok/s ยท cost` โ plus the model at
|
|
32
|
+
both ends of the block and on any turn that changed it, so the history reads
|
|
33
|
+
whether it is the whole session or a window of a long one:
|
|
34
|
+
`16:54 opencode-go` / `deepseek-v4.1-flash max` /
|
|
35
|
+
`in 1.7k (427.3k) ยท out 1.06k` / `hit 99.7% ยท 1.2k/s ยท $0.0006`.
|
|
36
|
+
- **The record** (server entry): every assistant step with tokens, cost,
|
|
37
|
+
timings, finish reason, the provider and model it ran as, and its session and
|
|
38
|
+
parent ids; prompts, session lifecycle and compaction events too.
|
|
39
|
+
- **Portable data** (from a checkout): `bun run report --ndjson` exports the
|
|
40
|
+
whole database, and `--import` merges it back on another machine.
|
|
41
|
+
- **Observational by design**: the plugin never rewrites prompts, requests or
|
|
42
|
+
responses, and a failure inside the recorder is swallowed โ telemetry can
|
|
43
|
+
never interrupt a session.
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- **OpenCode V2** (`>=2.0.0`). The V2 release changed the plugin API (slots,
|
|
48
|
+
panels, event stream).
|
|
49
|
+
- [Bun](https://bun.sh) to install dependencies (development only).
|
|
50
|
+
|
|
51
|
+
| | |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| **OS** | Linux, macOS and Windows. The database lives in the per-user state directory of each (table below); `test/paths.test.ts` asserts the path rules of all three from any machine, and `bun run check:platform` exercises the data layer โ paths, database, recorder, queries, panel assembly, dump round-trip, file permissions โ on the OS it runs on. CI runs the whole suite (including the TUI render tests) on Linux. |
|
|
54
|
+
| **Runtime** | Bun, the runtime OpenCode ships with. SQLite comes from `bun:sqlite`: there is no native module to build and nothing to compile on install. |
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
> [!IMPORTANT]
|
|
59
|
+
> **Not on the npm registry yet.** Until the first release is published,
|
|
60
|
+
> `opencode plugin add` and the package-name form below cannot resolve it โ use
|
|
61
|
+
> the local checkout, the last form of this section.
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
opencode plugin add opencode-usage-insights
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or add the package to `~/.config/opencode/opencode.jsonc` (server entry) and
|
|
68
|
+
`~/.config/opencode/cli.json` (TUI entry):
|
|
69
|
+
|
|
70
|
+
```jsonc
|
|
71
|
+
// ~/.config/opencode/opencode.jsonc
|
|
72
|
+
{
|
|
73
|
+
"$schema": "https://opencode.ai/config.json",
|
|
74
|
+
"plugins": ["opencode-usage-insights"]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
// ~/.config/opencode/cli.json
|
|
80
|
+
{
|
|
81
|
+
"$schema": "https://opencode.ai/v2/cli.json",
|
|
82
|
+
"plugins": ["opencode-usage-insights"]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To pass [options](#configuration), use the object form:
|
|
87
|
+
|
|
88
|
+
```jsonc
|
|
89
|
+
{
|
|
90
|
+
"plugins": [
|
|
91
|
+
{ "package": "opencode-usage-insights", "options": { "sidebarTurns": 5 } }
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Or register a local checkout. V2 resolves a local *directory* plugin through its
|
|
97
|
+
entry files, so pointing at `src/` loads the server recorder (`src/index.ts`)
|
|
98
|
+
and the TUI contributions (`src/tui.tsx`) from source:
|
|
99
|
+
|
|
100
|
+
```jsonc
|
|
101
|
+
// ~/.config/opencode/opencode.jsonc
|
|
102
|
+
{ "plugins": [{ "package": "/home/you/code/projects/opencode-usage-insights/src" }] }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```jsonc
|
|
106
|
+
// ~/.config/opencode/cli.json
|
|
107
|
+
{ "plugins": [{ "package": "/home/you/code/projects/opencode-usage-insights/src" }] }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Configuration changes reload on their own; after changing the plugin's source,
|
|
111
|
+
restart the background service with `opencode service restart`.
|
|
112
|
+
|
|
113
|
+
Send a prompt to verify: the telemetry line appears above the composer when the
|
|
114
|
+
turn finishes, and `/usage` (alias `/insights`) opens the panel.
|
|
115
|
+
|
|
116
|
+
## The line
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
14:32:07 ยท ฮ 3m12s ยท opencode-go/deepseek-v4.1-flash max ยท in 85.4k (1.62M), cached 1.53M, out 3.7k, cost $0.21 ยท sess in 2.1M (14.5M), cached 12.4M, out 88k, cost $1.90 ยท โ cache expired (โ11.7k)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
| Segment | Meaning |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `14:32:07` | When the response ended (`completed`, else `streamed`, else `created`). |
|
|
125
|
+
| `ฮ 3m12s` | Time since the previous prompt of the same session. |
|
|
126
|
+
| `opencode-go/deepseek-v4.1-flash max` | The identity of the turn: the provider the model ran through, as OpenCode is configured, and the model (the variant as a trailing word; `fields.model`/`fields.variant` hide either). Yellow with `changed` when the provider differs from the previous prompt, `model changed` when only the model does. |
|
|
127
|
+
| `in 85.4k (1.62M), cached 1.53M, out 3.7k, cost $0.21` | `in` is the uncached part of the prompt (fresh input + cache writes), the parenthesised value the whole prompt โ shown only when the two differ โ `cached` its cached part, `out` everything generated (visible output + reasoning), and `cost` what the turn was billed. With several steps the segment reads `3 steps ยท โฆ` and reports the step the turn ended with (`latest`) for the tokens, while the cost is always the turn's own โ every step is paid for; `turnTotals: "sum"` adds every step's tokens instead. |
|
|
128
|
+
| `cost $0.21` | Money, not tokens: a turn sums every step's cost whatever the token view shows. A cost of zero is left out, so a free model reads as free. `fields.cost: false` hides it. |
|
|
129
|
+
| `sess in 2.1M (14.5M), cached 12.4M, out 88k, cost $1.90` | The session totals reported by the host, in the same buckets, with the cost of the whole session. |
|
|
130
|
+
| `โ cache expired (โ11.7k)` | Yellow. The prompt cache read fell by that many tokens against a comparable previous step; `รN` when several steps in the turn lost cache. |
|
|
131
|
+
|
|
132
|
+
The turn segment carries no label by default โ the numbers are the turn's, and
|
|
133
|
+
`sess` marks the session's; `labels.turn` (e.g. `"turn"`) brings a prefix back.
|
|
134
|
+
|
|
135
|
+
A turn is every consecutive assistant step after a prompt. Compaction, shell,
|
|
136
|
+
`idle` and info messages do not split a turn; `user`, `synthetic` and `system`
|
|
137
|
+
messages do. From a checkout, `bun run demo` prints one example per case,
|
|
138
|
+
including the streaming one and both turn-total views.
|
|
139
|
+
|
|
140
|
+
## The panel
|
|
141
|
+
|
|
142
|
+
`/usage [view] [range]` opens the panel (`/usage` alone opens the last view, or
|
|
143
|
+
`overview` over `all` the first time). While the panel is open it owns input, so
|
|
144
|
+
the prompt cannot take the command again: `escape` closes it, `f` toggles full
|
|
145
|
+
screen, and the palette entry (`ctrl+p` โ "Usage insights") toggles it closed.
|
|
146
|
+
With the composer focused, `/usage` with no arguments also toggles it closed,
|
|
147
|
+
and any argument โ including an unknown token, which is ignored โ keeps it open.
|
|
148
|
+
There is no default key binding โ the command is palette-only, so it never
|
|
149
|
+
shadows a key you use.
|
|
150
|
+
|
|
151
|
+

|
|
152
|
+
|
|
153
|
+
The view bar at the top names every view and brackets the active one; `?` opens
|
|
154
|
+
a help overlay that repeats the keys and defines every number.
|
|
155
|
+
|
|
156
|
+
### Keys
|
|
157
|
+
|
|
158
|
+
| Key | Does |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| `โ` `โ` / `j` `k` | Move the cursor; the row under it is highlighted. |
|
|
161
|
+
| `pageup` `pagedown` / `home` `end` | Move by ten rows / to the first or last row. |
|
|
162
|
+
| `enter` | Drill into the row: a model โ the providers it ran through and their latency; a provider โ the models it served and its latency; a session โ its turns; a period โ the providers of the period. In `db`, prune the steps recorded before the selected retention's cutoff date (asks first). |
|
|
163
|
+
| `backspace` | Go back from a drill-down. |
|
|
164
|
+
| `1`โ`9` / `[` `]` | Pick a view (`9` is `db`) / cycle views. |
|
|
165
|
+
| `t` / `shift+t` | Next / previous range. |
|
|
166
|
+
| `s` / `shift+s` | Cycle the sort column / reverse it. |
|
|
167
|
+
| `b` | `periods` bucket: day โ week โ month. |
|
|
168
|
+
| `a` | `turns` scope: this session โ every session. |
|
|
169
|
+
| `m` `p` | Filter by model or by **provider** (a picker of the values actually observed). |
|
|
170
|
+
| `x` | Clear the filters. |
|
|
171
|
+
| `r` | Re-read the database. |
|
|
172
|
+
| `v` | `db`: compact the file, giving the space of deleted rows back. |
|
|
173
|
+
| `?` | Help overlay. |
|
|
174
|
+
| `f` / `escape` | Full screen / close. |
|
|
175
|
+
|
|
176
|
+
The view, range, sort, bucket, scope and filters are stored through the host's
|
|
177
|
+
durable storage, so the panel reopens where it was left.
|
|
178
|
+
|
|
179
|
+
### Views
|
|
180
|
+
|
|
181
|
+
| View | Answers |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `overview` | Steps, sessions, prompts, `in`/`cached`/`write`/`out`, `hit`, cost, an activity sparkline, a `this session` block and a **data coverage** block (how many models and providers were observed, and how many steps lack a model or a completion โ a zero elsewhere is only trustworthy when this is complete). |
|
|
184
|
+
| `models` | Every model with its cache (`hit` and the per-step `hit p50`), tokens, cost, the **providers it ran through** and how often it switched model or provider โ with the cache a provider switch cost. `enter` opens the per-provider detail. |
|
|
185
|
+
| `providers` | **Who ran the work**: one row per provider, with the models it served, steps, share, prompt, cached, `hit`, p50 throughput, wall time and cost. `enter` lists every model it served. Steps recorded without a provider (a model that was never observed) are dimmed and cannot be drilled into. |
|
|
186
|
+
| `cache` | Reuse by model, losses grouped by reason (`expired`, `compaction`, `unknown`), **cache survival against idle time**, reuse kept per provider, the observed TTL bounds (`hits up to` / `drops from`) and the most recent losses. |
|
|
187
|
+
| `periods` | Day, week or month totals โ steps, sessions, `in`, `cached`, `write`, `hit`, `out`, cost โ with a sparkline per metric and a totals row. `enter` lists the providers of a period. |
|
|
188
|
+
| `latency` | Wall time, streaming window and throughput as p50/p90/p99 overall and per model (per provider when a model is filtered). `n < 5` is dimmed. |
|
|
189
|
+
| `sessions` | Each session with steps, prompts, tokens, `hit`, cost, its directory and how many models it used; subagent sessions are indented under their parent and rolled up. |
|
|
190
|
+
| `turns` | The per-prompt history: one line per finished turn, rendered exactly like the live line. `a` widens it to every session, titled. |
|
|
191
|
+
| `db` | Where the file lives, what it holds and how much of it is old. The retention table lists, per preset (`30`/`90`/`365` days), how many steps a prune would remove and the cutoff date it prunes from โ pick one with `โ`/`โ` and `enter` prunes it (a confirmation first); `v` gives the space back. |
|
|
192
|
+
|
|
193
|
+
The panel sums the stored per-step buckets over the selected range, so a number
|
|
194
|
+
read in one place means the same in the other: `prompt` is everything the
|
|
195
|
+
provider saw (`cached + fresh + writes`), `in` the uncached part, `hit` is
|
|
196
|
+
`cached / prompt`, and `out` everything generated (visible output + reasoning).
|
|
197
|
+
|
|
198
|
+
The identity of a step is the **provider the model ran through** โ `opencode`,
|
|
199
|
+
`opencode-go`, `deepseek`, whatever the provider is called in the OpenCode
|
|
200
|
+
configuration โ and the model itself. That is what every view groups by, and it
|
|
201
|
+
is also the whole answer available: a gateway such as OpenCode Console may
|
|
202
|
+
re-route a request to another operator, but that upstream is not observable from
|
|
203
|
+
the plugin, and the identity a request *would* have been routed to is not
|
|
204
|
+
evidence of who ran it. The provider is: the same model through `deepseek` and
|
|
205
|
+
through `opencode-go` are two rows, two caches and two latency profiles, and a
|
|
206
|
+
switch between them forfeits the cache the previous one held (the switch counts
|
|
207
|
+
and the cache losses in the `models` view count exactly that).
|
|
208
|
+
|
|
209
|
+
`tps`/`wall`/`stream` are produced tokens per streaming second / request to
|
|
210
|
+
completed step / request to the last chunk. The sidebar's rate (per turn, and
|
|
211
|
+
the session average on its totals) is the **weighted** one โ produced tokens
|
|
212
|
+
divided by the summed streaming windows โ so it answers "how fast did this
|
|
213
|
+
produce"; the `latency` view reports the p50 of the per-step rates instead, which
|
|
214
|
+
answers "how fast is a typical step". A switch from one step to the next is not
|
|
215
|
+
by itself a fault โ moving to another model or provider is a deliberate choice โ
|
|
216
|
+
so the `models` view pairs the count with the cache the switch actually cost,
|
|
217
|
+
which only a provider switch can: two models never share a cache to lose. The
|
|
218
|
+
panel's `?` overlay defines every number in place.
|
|
219
|
+
|
|
220
|
+
## Configuration
|
|
221
|
+
|
|
222
|
+
Options apply to both entries (each side reads the ones it needs):
|
|
223
|
+
|
|
224
|
+
| Option | Default | Meaning |
|
|
225
|
+
| --- | --- | --- |
|
|
226
|
+
| `enabled` | `true` | `false` disables that side completely โ no line, no panel, no recording. |
|
|
227
|
+
| `db` | per-OS state path | SQLite path override. Must match between the server and TUI entries. |
|
|
228
|
+
| `turnTotals` | `"latest"` | How the turn segment counts prompt usage: `latest` shows the prompt size the turn ended with; `sum` adds every step of the turn. |
|
|
229
|
+
| `sidebar` | `true` | Show the session summary and the per-prompt history โ cache hit and cost per turn โ in the session sidebar (the slot is invisible while the sidebar is hidden). |
|
|
230
|
+
| `sidebarTurns` | `5` | How many turns the sidebar history shows. |
|
|
231
|
+
| `panel` | `true` | Show the `/usage` statistics panel and its command. `false` keeps the line and the sidebar. |
|
|
232
|
+
| `fields` | see below | Toggle line segments. |
|
|
233
|
+
| `labels` | see below | Override line labels (the plugin is English-only by default). |
|
|
234
|
+
|
|
235
|
+
### Fields and labels
|
|
236
|
+
|
|
237
|
+
```jsonc
|
|
238
|
+
{
|
|
239
|
+
"options": {
|
|
240
|
+
"fields": { "clock": true, "delta": true, "model": true, "variant": true, "turn": true, "session": true, "reasoning": false, "cost": true, "cacheExpiry": true },
|
|
241
|
+
"turnTotals": "latest",
|
|
242
|
+
"labels": { "delta": "ฮ", "turn": "", "session": "sess", "step": "step", "steps": "steps", "input": "in", "cached": "cached", "output": "out", "reasoning": "reason", "cost": "cost", "cacheExpired": "cache expired", "providerChanged": "changed", "modelChanged": "model changed" }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`fields: { "clock": false, "delta": false, "session": false }` turns the line
|
|
248
|
+
into a pure `provider/model ยท turn โฆ` reading.
|
|
249
|
+
|
|
250
|
+
## Database and privacy
|
|
251
|
+
|
|
252
|
+
The recorder writes to the per-user state directory:
|
|
253
|
+
|
|
254
|
+
| OS | Path |
|
|
255
|
+
| --- | --- |
|
|
256
|
+
| Linux / other | `$XDG_STATE_HOME/opencode/opencode-usage-insights/usage.db` (default `~/.local/state/opencode/opencode-usage-insights/usage.db`) |
|
|
257
|
+
| macOS | `~/Library/Application Support/opencode/opencode-usage-insights/usage.db` |
|
|
258
|
+
| Windows | `%LOCALAPPDATA%\opencode\opencode-usage-insights\usage.db` |
|
|
259
|
+
|
|
260
|
+
`OPENCODE_USAGE_INSIGHTS_DB=/path/to/usage.db` overrides the path, and the `db`
|
|
261
|
+
option overrides both. The file is private to the user: its directory is created
|
|
262
|
+
`0700` and the file `0600` (POSIX), re-applied on every open. The TUI reads
|
|
263
|
+
through a read-only connection, so it can never write, migrate or take the
|
|
264
|
+
recorder's lock.
|
|
265
|
+
|
|
266
|
+
Per step the database holds tokens, cost, timings, finish reason, the provider
|
|
267
|
+
and the model it ran as, and the session title and working directory. **Prompts
|
|
268
|
+
and responses are not stored**, and nothing leaves the machine. The schema, the
|
|
269
|
+
indexes, the identity rules and the security notes live in
|
|
270
|
+
[docs/internals.md](docs/internals.md).
|
|
271
|
+
|
|
272
|
+
## CLI report
|
|
273
|
+
|
|
274
|
+
From a checkout, the database can be read without opening the TUI:
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
bun run report # overview over all time
|
|
278
|
+
bun run report models 7d # one view, one range
|
|
279
|
+
bun run report all # every view
|
|
280
|
+
bun run report providers --model deepseek-v4.1-flash
|
|
281
|
+
bun run report periods --granularity month
|
|
282
|
+
bun run report models --provider opencode-go
|
|
283
|
+
bun run report --json # raw rows and every analysis, for scripting
|
|
284
|
+
bun run report --ndjson > backup.jsonl # export the whole database
|
|
285
|
+
bun run report --import backup.jsonl # merge an export (idempotent)
|
|
286
|
+
bun run report --help
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The views come from the same `buildPanel` the TUI renders, so the terminal and
|
|
290
|
+
the panel never drift apart. `--width`, `--granularity`, `--model`,
|
|
291
|
+
`--provider`, `--sort` and `--asc` mirror what the panel's keys do.
|
|
292
|
+
|
|
293
|
+
## Development
|
|
294
|
+
|
|
295
|
+
```sh
|
|
296
|
+
bun install
|
|
297
|
+
bun run typecheck # tsc --noEmit, strict
|
|
298
|
+
bun test # pure logic + database + TUI harness + render tests
|
|
299
|
+
bun run build # dist/index.js, dist/tui.js and declarations
|
|
300
|
+
bun run demo # print the line and every view from a seeded database
|
|
301
|
+
bun run docs:images # re-render docs/images/*.png (needs ImageMagick)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
- `src/core.ts`, `src/line.ts`, `src/panel-data.ts` and `src/stats/` are pure:
|
|
305
|
+
turn accounting, the host-shape adapter, the view models, and the database,
|
|
306
|
+
queries and analysis layer. No host imports, so they are unit-testable without
|
|
307
|
+
a TUI runtime.
|
|
308
|
+
- `src/text.ts` โ text hygiene and the token shape a provider id is validated
|
|
309
|
+
against.
|
|
310
|
+
- `src/recorder.ts` โ the event-stream state machine that fills the database.
|
|
311
|
+
- `src/panel-build.ts`, `src/panel.tsx`, `src/sidebar.tsx` โ panel assembly, the
|
|
312
|
+
panel component, and the sidebar contribution.
|
|
313
|
+
- `src/index.ts`, `src/tui.tsx` โ the server and TUI entries.
|
|
314
|
+
- `scripts/` โ the demo, report, bench, platform-check and documentation-image
|
|
315
|
+
CLIs, plus the two-entry `Bun.build`.
|
|
316
|
+
|
|
317
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the ground rules and review
|
|
318
|
+
expectations, and [docs/internals.md](docs/internals.md) for the database
|
|
319
|
+
design, performance rules and how the TUI side is tested.
|
|
320
|
+
|
|
321
|
+
## License
|
|
322
|
+
|
|
323
|
+
MIT โ see [LICENSE](./LICENSE). Author: Hugo Batista
|
|
324
|
+
(<https://github.com/hugobatista>).
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure turn accounting shared by the TUI line, the server recorder and the
|
|
3
|
+
* stats engine.
|
|
4
|
+
*
|
|
5
|
+
* Everything here operates on structural shapes that mirror the session
|
|
6
|
+
* messages published by OpenCode, so the logic stays testable without a host
|
|
7
|
+
* runtime and without the effect-based schema types.
|
|
8
|
+
*/
|
|
9
|
+
export type ModelRefLike = {
|
|
10
|
+
providerID: string;
|
|
11
|
+
id: string;
|
|
12
|
+
variant?: string | undefined;
|
|
13
|
+
};
|
|
14
|
+
export type AssistantMessage = {
|
|
15
|
+
type: "assistant";
|
|
16
|
+
id: string;
|
|
17
|
+
time?: {
|
|
18
|
+
created?: number | undefined;
|
|
19
|
+
streamed?: number | undefined;
|
|
20
|
+
completed?: number | undefined;
|
|
21
|
+
} | undefined;
|
|
22
|
+
model?: ModelRefLike | undefined;
|
|
23
|
+
tokens?: {
|
|
24
|
+
input?: number | undefined;
|
|
25
|
+
output?: number | undefined;
|
|
26
|
+
reasoning?: number | undefined;
|
|
27
|
+
cache?: {
|
|
28
|
+
read?: number | undefined;
|
|
29
|
+
write?: number | undefined;
|
|
30
|
+
} | undefined;
|
|
31
|
+
} | undefined;
|
|
32
|
+
cost?: number | undefined;
|
|
33
|
+
finish?: string | null | undefined;
|
|
34
|
+
rawFinish?: string | null | undefined;
|
|
35
|
+
};
|
|
36
|
+
export type PromptMessage = {
|
|
37
|
+
type: "user" | "synthetic" | "system";
|
|
38
|
+
id?: string | undefined;
|
|
39
|
+
time?: {
|
|
40
|
+
created?: number | undefined;
|
|
41
|
+
} | undefined;
|
|
42
|
+
};
|
|
43
|
+
export type TranscriptMessage = AssistantMessage | PromptMessage | {
|
|
44
|
+
type: string;
|
|
45
|
+
id?: string | undefined;
|
|
46
|
+
time?: {
|
|
47
|
+
created?: number | undefined;
|
|
48
|
+
} | undefined;
|
|
49
|
+
};
|
|
50
|
+
/** Accumulated token usage for a group of assistant steps. */
|
|
51
|
+
export type Totals = {
|
|
52
|
+
input: number;
|
|
53
|
+
cached: number;
|
|
54
|
+
output: number;
|
|
55
|
+
reasoning: number;
|
|
56
|
+
cacheWrite: number;
|
|
57
|
+
cost: number;
|
|
58
|
+
steps: number;
|
|
59
|
+
};
|
|
60
|
+
export type CacheDrop = {
|
|
61
|
+
messageID: string;
|
|
62
|
+
/** Tokens that were expected to hit the prompt cache but did not. */
|
|
63
|
+
tokens: number;
|
|
64
|
+
/** `cache.read` of the step that was used as the reuse reference. */
|
|
65
|
+
prevCached: number;
|
|
66
|
+
/** Time between the reference step's completion and this step's request. */
|
|
67
|
+
gapMs: number | undefined;
|
|
68
|
+
};
|
|
69
|
+
export type TurnStats = {
|
|
70
|
+
/** Wall clock time of the last step's completion. */
|
|
71
|
+
endAt: number | undefined;
|
|
72
|
+
/** Creation time of the first settled step of the turn. */
|
|
73
|
+
startAt: number | undefined;
|
|
74
|
+
/** Creation time of the prompt that opened this turn. */
|
|
75
|
+
promptAt: number | undefined;
|
|
76
|
+
/** Time between the previous prompt and this turn's prompt. */
|
|
77
|
+
promptGapMs: number | undefined;
|
|
78
|
+
providerID: string | undefined;
|
|
79
|
+
modelID: string | undefined;
|
|
80
|
+
variant: string | undefined;
|
|
81
|
+
/** Provider ID differs from the previous turn in the same transcript. */
|
|
82
|
+
providerChanged: boolean;
|
|
83
|
+
/** Provider is the same but the model or variant differs. */
|
|
84
|
+
modelChanged: boolean;
|
|
85
|
+
totals: Totals;
|
|
86
|
+
/** Passed through from the caller (session-wide accumulated usage). */
|
|
87
|
+
session: Totals | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Prompt-side usage of the last finished step: the context the turn ended
|
|
90
|
+
* with. Summing `input`/`cached` over the steps of a turn counts the same
|
|
91
|
+
* re-sent prompt once per step, which is not what a context view means.
|
|
92
|
+
*/
|
|
93
|
+
latest: Totals;
|
|
94
|
+
drops: CacheDrop[];
|
|
95
|
+
cacheDropTokens: number;
|
|
96
|
+
cacheExpired: boolean;
|
|
97
|
+
/** Assistant steps in the turn. */
|
|
98
|
+
steps: number;
|
|
99
|
+
};
|
|
100
|
+
export type TurnSegment = {
|
|
101
|
+
assistants: AssistantMessage[];
|
|
102
|
+
prompt: TranscriptMessage | undefined;
|
|
103
|
+
};
|
|
104
|
+
/** Message types that open a turn: a run of assistants belongs to the last one. */
|
|
105
|
+
export declare const PROMPT_BOUNDARIES: ReadonlySet<string>;
|
|
106
|
+
/**
|
|
107
|
+
* Split a transcript into turns. A turn is a run of assistant steps opened by
|
|
108
|
+
* a prompt boundary (`user`, `synthetic`, `system`). Idle notices, info
|
|
109
|
+
* messages, compactions and shell entries keep the current turn open so a
|
|
110
|
+
* single execution is never split in two.
|
|
111
|
+
*/
|
|
112
|
+
export declare function transcriptSegments(messages: readonly TranscriptMessage[]): TurnSegment[];
|
|
113
|
+
/**
|
|
114
|
+
* The last `count` segments, found by walking the transcript backwards. The
|
|
115
|
+
* live line only needs the turn that just ended and the one before it, and a
|
|
116
|
+
* long session holds tens of thousands of messages: the tail walk stops as
|
|
117
|
+
* soon as it has them, instead of building a segment for every turn.
|
|
118
|
+
*/
|
|
119
|
+
export declare function transcriptTail(messages: readonly TranscriptMessage[], count: number): TurnSegment[];
|
|
120
|
+
export declare function tokenCounts(message: AssistantMessage): {
|
|
121
|
+
input: number;
|
|
122
|
+
output: number;
|
|
123
|
+
reasoning: number;
|
|
124
|
+
read: number;
|
|
125
|
+
write: number;
|
|
126
|
+
};
|
|
127
|
+
export declare function totalize(steps: readonly AssistantMessage[]): Totals;
|
|
128
|
+
/**
|
|
129
|
+
* The identity of a model reference: the provider it runs through, the model
|
|
130
|
+
* and its variant. Two steps with the same key ran the same thing the same way,
|
|
131
|
+
* which is what `modelChanged` compares.
|
|
132
|
+
*/
|
|
133
|
+
export declare function identityKey(model: ModelRefLike | undefined): string | undefined;
|
|
134
|
+
export declare function cacheRead(message: AssistantMessage | undefined): number;
|
|
135
|
+
export declare function stepCreated(message: AssistantMessage | undefined): number | undefined;
|
|
136
|
+
export declare function stepEnd(message: AssistantMessage | undefined): number | undefined;
|
|
137
|
+
/**
|
|
138
|
+
* Which identity the two steps must share for a reuse loss to be measurable.
|
|
139
|
+
* `provider` is the strict one: two providers are two caches, so a switch
|
|
140
|
+
* between them is not a loss but a different cache. `model` drops that test,
|
|
141
|
+
* for the reports that measure what an identity change costs: forfeiting the
|
|
142
|
+
* previous provider's cache is exactly the cost they report.
|
|
143
|
+
*/
|
|
144
|
+
export type CacheDropScope = "provider" | "model";
|
|
145
|
+
/**
|
|
146
|
+
* Cache reuse loss against a reference step, mirroring OpenCode's own
|
|
147
|
+
* `turn-usage` heuristic: only comparable when the model is identical, and
|
|
148
|
+
* OpenAI's API reports small reads that are not worth flagging.
|
|
149
|
+
*/
|
|
150
|
+
export declare function cacheDrop(reference: AssistantMessage | undefined, current: AssistantMessage, scope?: CacheDropScope): number | undefined;
|
|
151
|
+
/** Milliseconds between a reference step's completion and the next request. */
|
|
152
|
+
export declare function stepGap(reference: AssistantMessage | undefined, current: AssistantMessage): number | undefined;
|
|
153
|
+
/**
|
|
154
|
+
* Streaming duration of a step: request start to the last streamed chunk. The
|
|
155
|
+
* host's `streamed` stamp marks the end of streaming, so this is the window a
|
|
156
|
+
* generation rate belongs to โ not a time to first token, which the session
|
|
157
|
+
* messages do not record.
|
|
158
|
+
*/
|
|
159
|
+
export declare function stepStreamMs(message: AssistantMessage): number | undefined;
|
|
160
|
+
/**
|
|
161
|
+
* Tokens produced per second over a window. For a step the window is the
|
|
162
|
+
* streaming one: request start to the last streamed chunk (`created` โ
|
|
163
|
+
* `streamed`) โ `completed` marks the end of the step, which for a tool-call
|
|
164
|
+
* step happens after the tool calls were handled. Returns undefined when the
|
|
165
|
+
* window or the produced tokens are unknown. Single definition, shared by the
|
|
166
|
+
* recorder (which stores it per step) and the latency view.
|
|
167
|
+
*/
|
|
168
|
+
export declare function tokensPerSecond(produced: number, from: number | undefined, to: number | undefined): number | undefined;
|
|
169
|
+
/** Output-plus-reasoning tokens per second of a step. */
|
|
170
|
+
export declare function stepTps(message: AssistantMessage): number | undefined;
|
|
171
|
+
/**
|
|
172
|
+
* True when a step has final numbers. A step that is still streaming reports
|
|
173
|
+
* partial tokens (usually zero) and no cache read yet, so reading it would
|
|
174
|
+
* render an empty line and invent a cache-expiry warning against it.
|
|
175
|
+
*/
|
|
176
|
+
export declare function isSettledStep(message: AssistantMessage): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Compute the stats of the most recent turn in a transcript, including the
|
|
179
|
+
* comparison against the previous turn (a provider or model switch, and cache
|
|
180
|
+
* reuse).
|
|
181
|
+
*
|
|
182
|
+
* Returns undefined while the newest turn is still being generated: the line
|
|
183
|
+
* appears when a turn ends, never with the zeroes of an in-flight response.
|
|
184
|
+
*/
|
|
185
|
+
export declare function computeTurn(messages: readonly TranscriptMessage[], session?: Totals): TurnStats | undefined;
|
|
186
|
+
/** True when a cache drop is small enough to be provider noise. */
|
|
187
|
+
export declare function isNoiseDrop(providerID: string | undefined, drop: number): boolean;
|
|
188
|
+
export type LineTone = "base" | "muted" | "warning";
|
|
189
|
+
export type LineSegment = {
|
|
190
|
+
text: string;
|
|
191
|
+
tone: LineTone;
|
|
192
|
+
};
|
|
193
|
+
export type LineLabels = {
|
|
194
|
+
delta: string;
|
|
195
|
+
turn: string;
|
|
196
|
+
session: string;
|
|
197
|
+
step: string;
|
|
198
|
+
steps: string;
|
|
199
|
+
input: string;
|
|
200
|
+
cached: string;
|
|
201
|
+
output: string;
|
|
202
|
+
reasoning: string;
|
|
203
|
+
cost: string;
|
|
204
|
+
cacheExpired: string;
|
|
205
|
+
providerChanged: string;
|
|
206
|
+
modelChanged: string;
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* How the turn segment counts prompt usage. `latest` shows the prompt size the
|
|
210
|
+
* turn ended with โ the context view, and what OpenCode's own turn-token debug
|
|
211
|
+
* line reports. `sum` adds every step of the turn: the billing view, where a
|
|
212
|
+
* tool-call loop re-sends (and re-reads) the same context once per step.
|
|
213
|
+
*/
|
|
214
|
+
export type TurnTotalsView = "latest" | "sum";
|
|
215
|
+
export type LineFields = {
|
|
216
|
+
clock: boolean;
|
|
217
|
+
delta: boolean;
|
|
218
|
+
model: boolean;
|
|
219
|
+
variant: boolean;
|
|
220
|
+
turn: boolean;
|
|
221
|
+
session: boolean;
|
|
222
|
+
reasoning: boolean;
|
|
223
|
+
cost: boolean;
|
|
224
|
+
cacheExpiry: boolean;
|
|
225
|
+
};
|
|
226
|
+
export declare const DEFAULT_LABELS: LineLabels;
|
|
227
|
+
export declare const DEFAULT_FIELDS: LineFields;
|
|
228
|
+
export declare function formatClock(ms: number): string;
|
|
229
|
+
export declare function formatDuration(ms: number): string;
|
|
230
|
+
export declare function formatTokens(value: number): string;
|
|
231
|
+
export declare function formatCost(value: number): string;
|
|
232
|
+
/**
|
|
233
|
+
* The buckets the line displays, derived from a totals group. `input` is the
|
|
234
|
+
* *uncached* part of the prompt (fresh input plus cache writes, i.e. what the
|
|
235
|
+
* debug line's `new` counts on the prompt side), `prompt` is the whole prompt
|
|
236
|
+
* of the request and `output` is everything the model generated (visible output
|
|
237
|
+
* + reasoning). OpenCode's own debug line reconciles with these:
|
|
238
|
+
* `new = in + out` and `total = prompt + out`, with `prompt = in + cached`.
|
|
239
|
+
* The stored buckets stay raw; only the display folds.
|
|
240
|
+
*/
|
|
241
|
+
export type DisplayTotals = Totals & {
|
|
242
|
+
/** Whole prompt of the request: fresh input + cached reads + cache writes. */
|
|
243
|
+
prompt: number;
|
|
244
|
+
};
|
|
245
|
+
export declare function displayTotals(totals: Totals): DisplayTotals;
|
|
246
|
+
/**
|
|
247
|
+
* Render the telemetry line as toned segments. The caller maps tones onto
|
|
248
|
+
* theme colors; `formatLine` joins the segments as plain text.
|
|
249
|
+
*/
|
|
250
|
+
export declare function formatLineSegments(stats: TurnStats, options?: {
|
|
251
|
+
labels?: Partial<LineLabels>;
|
|
252
|
+
fields?: Partial<LineFields>;
|
|
253
|
+
turnTotals?: TurnTotalsView;
|
|
254
|
+
}): LineSegment[];
|
|
255
|
+
export declare function formatLine(stats: TurnStats, options?: {
|
|
256
|
+
labels?: Partial<LineLabels>;
|
|
257
|
+
fields?: Partial<LineFields>;
|
|
258
|
+
turnTotals?: TurnTotalsView;
|
|
259
|
+
}): string;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Plugin } from "@opencode/plugin";
|
|
2
|
+
/**
|
|
3
|
+
* Server-side recorder. Subscribes to the session event stream and persists
|
|
4
|
+
* every assistant step, prompt and lifecycle event into the stats database;
|
|
5
|
+
* the TUI entry reads the same database for its live line and stats panel.
|
|
6
|
+
*
|
|
7
|
+
* Everything the recorder stores comes from the event stream โ the provider
|
|
8
|
+
* and model a step ran as, the tokens and the timing. The plugin is
|
|
9
|
+
* observational: it never rewrites prompts, requests or responses, registers
|
|
10
|
+
* no hook on the request path, and a failure inside the recorder is swallowed
|
|
11
|
+
* so a session can never be interrupted by its own telemetry.
|
|
12
|
+
*/
|
|
13
|
+
export type Options = {
|
|
14
|
+
/** Turn recording off: no event subscription, no database. */
|
|
15
|
+
enabled?: boolean | undefined;
|
|
16
|
+
/** SQLite database path override (defaults to the per-OS state directory). */
|
|
17
|
+
db?: string | undefined;
|
|
18
|
+
};
|
|
19
|
+
declare const _default: Plugin.Plugin;
|
|
20
|
+
export default _default;
|