@leo000001/opencode-quota-sidebar 1.13.8 → 1.13.10
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.md +480 -484
- package/dist/cost.d.ts +2 -3
- package/dist/cost.js +4 -24
- package/dist/usage_service.js +8 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,484 +1,480 @@
|
|
|
1
|
-
# opencode-quota-sidebar
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@leo000001/opencode-quota-sidebar)
|
|
4
|
-
[](https://github.com/xihuai18/opencode-quota-sidebar/blob/main/LICENSE)
|
|
5
|
-
|
|
6
|
-
OpenCode plugin: show token usage and subscription quota in the session sidebar title.
|
|
7
|
-
|
|
8
|
-

|
|
9
|
-
|
|
10
|
-
## Install
|
|
11
|
-
|
|
12
|
-
Add the package name to `plugin` in your `opencode.json`. OpenCode uses Bun to install it automatically on startup:
|
|
13
|
-
|
|
14
|
-
```json
|
|
15
|
-
{
|
|
16
|
-
"plugin": ["@leo000001/opencode-quota-sidebar@1.13.2"]
|
|
17
|
-
}
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Note for OpenCode `>=1.2.15`: TUI settings (`theme`/`keybinds`/`tui`) moved to `tui.json`, but plugin loading still stays in `opencode.json` (`plugin: []`).
|
|
21
|
-
This plugin also accepts both `config.providers` and older `provider.list` runtime shapes when discovering provider options.
|
|
22
|
-
|
|
23
|
-
If you prefer automatic upgrades, you can still use `@latest`, but pinning an exact version makes behavior easier to reproduce when debugging.
|
|
24
|
-
|
|
25
|
-
## Development (build from source)
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npm install
|
|
29
|
-
npm run build
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Add the built file to your `opencode.json`:
|
|
33
|
-
|
|
34
|
-
```json
|
|
35
|
-
{
|
|
36
|
-
"plugin": ["file:///ABSOLUTE/PATH/opencode-quota-sidebar/dist/index.js"]
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
On Windows, use forward slashes: `"file:///D:/Lab/opencode-quota-sidebar/dist/index.js"`
|
|
41
|
-
|
|
42
|
-
## Supported quota providers
|
|
43
|
-
|
|
44
|
-
| Provider | Endpoint | Auth | Status |
|
|
45
|
-
| -------------- | -------------------------------------- | --------------- | --------------------------------------- |
|
|
46
|
-
| OpenAI Codex | `chatgpt.com/backend-api/wham/usage` | OAuth (ChatGPT) | Multi-window (short-term + weekly) |
|
|
47
|
-
| GitHub Copilot | `api.github.com/copilot_internal/user` | OAuth | Monthly quota |
|
|
48
|
-
| RightCode | `www.right.codes/account/summary` | API key | Subscription or balance (by prefix) |
|
|
49
|
-
| Buzz | `buzzai.cc/v1/dashboard/billing/*` | API key | Balance only (computed from total-used) |
|
|
50
|
-
| Anthropic | `api.anthropic.com/api/oauth/usage` | OAuth | Multi-window (5h + weekly / plan-based) |
|
|
51
|
-
|
|
52
|
-
Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware AI, etc.)? See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
53
|
-
|
|
54
|
-
## Features
|
|
55
|
-
|
|
56
|
-
- Session title becomes multiline in sidebar:
|
|
57
|
-
- line 1: original session title
|
|
58
|
-
- line 2: Input/Output tokens
|
|
59
|
-
- line 3: Cache Read tokens (only if non-zero)
|
|
60
|
-
- line 4: Cache Write tokens (only if non-zero)
|
|
61
|
-
- line 5: `$X.XX as API cost` (equivalent API billing for subscription-auth providers)
|
|
62
|
-
- quota lines: quota text like `OpenAI 5h 80% Rst 16:20`; short windows (`5h`, `1d`, `Daily`) show `HH:MM` on same-day resets and `MM-DD HH:MM` when crossing days, while longer windows continue to show `MM-DD`
|
|
63
|
-
- RightCode daily quota shows `$remaining/$dailyTotal` + expiry (e.g. `RC Daily $105/$60 Exp 02-27`, without trailing percent) and also shows balance on the next indented line when available; `Exp` remains date-only
|
|
64
|
-
- Session-scoped usage/quota can include descendant subagent sessions (enabled by default via `sidebar.includeChildren=true`). Traversal is bounded by `childrenMaxDepth` (default 6), `childrenMaxSessions` (default 128), and `childrenConcurrency` (default 5); truncation is logged when `OPENCODE_QUOTA_DEBUG=1`. Day/week/month ranges never merge children — only session scope does.
|
|
65
|
-
- Toast message includes three sections: `Token Usage`, `Cost as API` (per provider), and `Quota`
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
- Quota
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
-
|
|
90
|
-
- `
|
|
91
|
-
- `
|
|
92
|
-
- `
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
"description": "Show
|
|
161
|
-
"template": "Call tool quota_summary with period=
|
|
162
|
-
},
|
|
163
|
-
"
|
|
164
|
-
"description": "Show this
|
|
165
|
-
"template": "Call tool quota_summary with period=
|
|
166
|
-
},
|
|
167
|
-
"
|
|
168
|
-
"description": "
|
|
169
|
-
"template": "Call tool
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
-
|
|
191
|
-
|
|
192
|
-
Optional
|
|
193
|
-
|
|
194
|
-
- `
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
- `sidebar.
|
|
220
|
-
- `sidebar.
|
|
221
|
-
- `sidebar.
|
|
222
|
-
- `sidebar.
|
|
223
|
-
- `sidebar.
|
|
224
|
-
- `sidebar.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
- `
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
- `quota.
|
|
233
|
-
- `quota.
|
|
234
|
-
- `quota.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
- `
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
"
|
|
259
|
-
"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
"
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
- `sidebar.
|
|
287
|
-
- `
|
|
288
|
-
- `sidebar.
|
|
289
|
-
- `sidebar.
|
|
290
|
-
- `sidebar.
|
|
291
|
-
- `
|
|
292
|
-
-
|
|
293
|
-
- `
|
|
294
|
-
- `
|
|
295
|
-
- `
|
|
296
|
-
- API
|
|
297
|
-
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
-
|
|
453
|
-
-
|
|
454
|
-
-
|
|
455
|
-
|
|
456
|
-
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
- The `
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
##
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
## License
|
|
483
|
-
|
|
484
|
-
MIT. See `LICENSE`.
|
|
1
|
+
# opencode-quota-sidebar
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@leo000001/opencode-quota-sidebar)
|
|
4
|
+
[](https://github.com/xihuai18/opencode-quota-sidebar/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
OpenCode plugin: show token usage and subscription quota in the session sidebar title.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Add the package name to `plugin` in your `opencode.json`. OpenCode uses Bun to install it automatically on startup:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"plugin": ["@leo000001/opencode-quota-sidebar@1.13.2"]
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Note for OpenCode `>=1.2.15`: TUI settings (`theme`/`keybinds`/`tui`) moved to `tui.json`, but plugin loading still stays in `opencode.json` (`plugin: []`).
|
|
21
|
+
This plugin also accepts both `config.providers` and older `provider.list` runtime shapes when discovering provider options.
|
|
22
|
+
|
|
23
|
+
If you prefer automatic upgrades, you can still use `@latest`, but pinning an exact version makes behavior easier to reproduce when debugging.
|
|
24
|
+
|
|
25
|
+
## Development (build from source)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install
|
|
29
|
+
npm run build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Add the built file to your `opencode.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"plugin": ["file:///ABSOLUTE/PATH/opencode-quota-sidebar/dist/index.js"]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
On Windows, use forward slashes: `"file:///D:/Lab/opencode-quota-sidebar/dist/index.js"`
|
|
41
|
+
|
|
42
|
+
## Supported quota providers
|
|
43
|
+
|
|
44
|
+
| Provider | Endpoint | Auth | Status |
|
|
45
|
+
| -------------- | -------------------------------------- | --------------- | --------------------------------------- |
|
|
46
|
+
| OpenAI Codex | `chatgpt.com/backend-api/wham/usage` | OAuth (ChatGPT) | Multi-window (short-term + weekly) |
|
|
47
|
+
| GitHub Copilot | `api.github.com/copilot_internal/user` | OAuth | Monthly quota |
|
|
48
|
+
| RightCode | `www.right.codes/account/summary` | API key | Subscription or balance (by prefix) |
|
|
49
|
+
| Buzz | `buzzai.cc/v1/dashboard/billing/*` | API key | Balance only (computed from total-used) |
|
|
50
|
+
| Anthropic | `api.anthropic.com/api/oauth/usage` | OAuth | Multi-window (5h + weekly / plan-based) |
|
|
51
|
+
|
|
52
|
+
Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware AI, etc.)? See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
- Session title becomes multiline in sidebar:
|
|
57
|
+
- line 1: original session title
|
|
58
|
+
- line 2: Input/Output tokens
|
|
59
|
+
- line 3: Cache Read tokens (only if non-zero)
|
|
60
|
+
- line 4: Cache Write tokens (only if non-zero)
|
|
61
|
+
- line 5: `$X.XX as API cost` (equivalent API billing for subscription-auth providers)
|
|
62
|
+
- quota lines: quota text like `OpenAI 5h 80% Rst 16:20`; short windows (`5h`, `1d`, `Daily`) show `HH:MM` on same-day resets and `MM-DD HH:MM` when crossing days, while longer windows continue to show `MM-DD`
|
|
63
|
+
- RightCode daily quota shows `$remaining/$dailyTotal` + expiry (e.g. `RC Daily $105/$60 Exp 02-27`, without trailing percent) and also shows balance on the next indented line when available; `Exp` remains date-only
|
|
64
|
+
- Session-scoped usage/quota can include descendant subagent sessions (enabled by default via `sidebar.includeChildren=true`). Traversal is bounded by `childrenMaxDepth` (default 6), `childrenMaxSessions` (default 128), and `childrenConcurrency` (default 5); truncation is logged when `OPENCODE_QUOTA_DEBUG=1`. Day/week/month ranges never merge children — only session scope does.
|
|
65
|
+
- Toast message includes three sections: `Token Usage`, `Cost as API` (per provider), and `Quota`
|
|
66
|
+
- Quota snapshots are de-duplicated before rendering to avoid repeated provider lines
|
|
67
|
+
- Custom tools:
|
|
68
|
+
- `quota_summary` — generate usage report for session/day/week/month (markdown + toast)
|
|
69
|
+
- `quota_show` — toggle sidebar title display on/off (state persists across sessions)
|
|
70
|
+
- Quota connectors:
|
|
71
|
+
- OpenAI Codex OAuth (`/backend-api/wham/usage`)
|
|
72
|
+
- GitHub Copilot OAuth (`/copilot_internal/user`)
|
|
73
|
+
- RightCode API key (`/account/summary`)
|
|
74
|
+
- Buzz API key (`/v1/dashboard/billing/subscription` + `/v1/dashboard/billing/usage`)
|
|
75
|
+
- Anthropic Claude OAuth (`/api/oauth/usage`, with beta header)
|
|
76
|
+
- OpenAI OAuth quota checks auto-refresh expired access token (using refresh token)
|
|
77
|
+
- API key providers still show usage aggregation (quota only applies to subscription providers)
|
|
78
|
+
- Incremental usage aggregation — only processes new messages since last cursor
|
|
79
|
+
- Sidebar token units are adaptive (`k`/`m` with one decimal where applicable)
|
|
80
|
+
|
|
81
|
+
## Storage layout
|
|
82
|
+
|
|
83
|
+
The plugin stores lightweight global state and date-partitioned session chunks.
|
|
84
|
+
|
|
85
|
+
- Global metadata: `<opencode-data>/quota-sidebar.state.json`
|
|
86
|
+
- `titleEnabled`
|
|
87
|
+
- `sessionDateMap` (sessionID -> `YYYY-MM-DD`)
|
|
88
|
+
- `quotaCache`
|
|
89
|
+
- Session chunks: `<opencode-data>/quota-sidebar-sessions/YYYY/MM/DD.json`
|
|
90
|
+
- per-session title state (`baseTitle`, `lastAppliedTitle`)
|
|
91
|
+
- `createdAt`
|
|
92
|
+
- `parentID` (when the session is a subagent child session)
|
|
93
|
+
- cached usage summary used by `quota_summary`
|
|
94
|
+
- incremental aggregation cursor
|
|
95
|
+
|
|
96
|
+
Example tree:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
~/.local/share/opencode/
|
|
100
|
+
quota-sidebar.state.json
|
|
101
|
+
quota-sidebar-sessions/
|
|
102
|
+
2026/
|
|
103
|
+
02/
|
|
104
|
+
23.json
|
|
105
|
+
24.json
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Sessions older than `retentionDays` (default 730 days / 2 years) are evicted from
|
|
109
|
+
memory on startup. Chunk files remain on disk for historical range scans.
|
|
110
|
+
|
|
111
|
+
## Compatibility
|
|
112
|
+
|
|
113
|
+
- Node.js: >= 18 (for `fetch` + `AbortController`)
|
|
114
|
+
- OpenCode: plugin SDK `@opencode-ai/plugin` ^1.2.10
|
|
115
|
+
- OpenCode config split: if you are on `>=1.2.15`, keep this plugin in `opencode.json` and keep TUI-only keys in `tui.json`.
|
|
116
|
+
|
|
117
|
+
## Force refresh after npm update
|
|
118
|
+
|
|
119
|
+
If `npm view @leo000001/opencode-quota-sidebar version` shows a newer version but OpenCode still behaves like an older release, OpenCode/Bun is usually reusing an older installed copy.
|
|
120
|
+
|
|
121
|
+
Recommended recovery steps:
|
|
122
|
+
|
|
123
|
+
1. Pin the target plugin version in `opencode.json`.
|
|
124
|
+
2. Fully exit OpenCode.
|
|
125
|
+
3. Delete any cached installed copies of the plugin.
|
|
126
|
+
4. Start OpenCode again so it reinstalls the package.
|
|
127
|
+
5. Verify the actual installed `package.json` version under the plugin directory.
|
|
128
|
+
|
|
129
|
+
Common install/cache locations:
|
|
130
|
+
|
|
131
|
+
- `~/.cache/opencode/node_modules/@leo000001/opencode-quota-sidebar`
|
|
132
|
+
- `~/node_modules/@leo000001/opencode-quota-sidebar`
|
|
133
|
+
|
|
134
|
+
Windows PowerShell example:
|
|
135
|
+
|
|
136
|
+
```powershell
|
|
137
|
+
Remove-Item -Recurse -Force "$HOME\.cache\opencode\node_modules\@leo000001\opencode-quota-sidebar" -ErrorAction SilentlyContinue
|
|
138
|
+
Remove-Item -Recurse -Force "$HOME\node_modules\@leo000001\opencode-quota-sidebar" -ErrorAction SilentlyContinue
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
macOS / Linux example:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
rm -rf ~/.cache/opencode/node_modules/@leo000001/opencode-quota-sidebar
|
|
145
|
+
rm -rf ~/node_modules/@leo000001/opencode-quota-sidebar
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Optional commands
|
|
149
|
+
|
|
150
|
+
You can add these command templates in `opencode.json` so you can run `/qday`, `/qweek`, `/qmonth`, `/qtoggle`:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"command": {
|
|
155
|
+
"qday": {
|
|
156
|
+
"description": "Show today's usage and quota",
|
|
157
|
+
"template": "Call tool quota_summary with period=day and toast=true."
|
|
158
|
+
},
|
|
159
|
+
"qweek": {
|
|
160
|
+
"description": "Show this week's usage and quota",
|
|
161
|
+
"template": "Call tool quota_summary with period=week and toast=true."
|
|
162
|
+
},
|
|
163
|
+
"qmonth": {
|
|
164
|
+
"description": "Show this month's usage and quota",
|
|
165
|
+
"template": "Call tool quota_summary with period=month and toast=true."
|
|
166
|
+
},
|
|
167
|
+
"qtoggle": {
|
|
168
|
+
"description": "Toggle sidebar usage display on/off",
|
|
169
|
+
"template": "Call tool quota_show (no arguments, it toggles)."
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Configuration files
|
|
176
|
+
|
|
177
|
+
Recommended global config:
|
|
178
|
+
|
|
179
|
+
- `~/.config/opencode/quota-sidebar.config.json`
|
|
180
|
+
|
|
181
|
+
Optional project overrides:
|
|
182
|
+
|
|
183
|
+
- `<worktree>/quota-sidebar.config.json`
|
|
184
|
+
- `<directory>/quota-sidebar.config.json` (when different from `worktree`)
|
|
185
|
+
- `<worktree>/.opencode/quota-sidebar.config.json`
|
|
186
|
+
- `<directory>/.opencode/quota-sidebar.config.json` (when different from `worktree`)
|
|
187
|
+
|
|
188
|
+
Optional explicit override:
|
|
189
|
+
|
|
190
|
+
- `OPENCODE_QUOTA_CONFIG=/absolute/path/to/config.json`
|
|
191
|
+
|
|
192
|
+
Optional config-home override:
|
|
193
|
+
|
|
194
|
+
- `OPENCODE_QUOTA_CONFIG_HOME=/absolute/path/to/config-home`
|
|
195
|
+
|
|
196
|
+
Resolution order (low -> high):
|
|
197
|
+
|
|
198
|
+
1. Global config (`~/.config/opencode/...`)
|
|
199
|
+
2. `<worktree>/quota-sidebar.config.json`
|
|
200
|
+
3. `<directory>/quota-sidebar.config.json`
|
|
201
|
+
4. `<worktree>/.opencode/quota-sidebar.config.json`
|
|
202
|
+
5. `<directory>/.opencode/quota-sidebar.config.json`
|
|
203
|
+
6. `OPENCODE_QUOTA_CONFIG`
|
|
204
|
+
|
|
205
|
+
Values are layered; later sources override earlier ones.
|
|
206
|
+
|
|
207
|
+
## Configuration
|
|
208
|
+
|
|
209
|
+
If you do not provide any config file, the plugin uses the built-in defaults below.
|
|
210
|
+
|
|
211
|
+
### Built-in defaults
|
|
212
|
+
|
|
213
|
+
Sidebar defaults:
|
|
214
|
+
|
|
215
|
+
- `sidebar.enabled`: `true`
|
|
216
|
+
- `sidebar.width`: `36` (clamped to `20`-`60`)
|
|
217
|
+
- `sidebar.multilineTitle`: `true`
|
|
218
|
+
- `sidebar.showCost`: `true`
|
|
219
|
+
- `sidebar.showQuota`: `true`
|
|
220
|
+
- `sidebar.wrapQuotaLines`: `true`
|
|
221
|
+
- `sidebar.includeChildren`: `true`
|
|
222
|
+
- `sidebar.childrenMaxDepth`: `6` (clamped to `1`-`32`)
|
|
223
|
+
- `sidebar.childrenMaxSessions`: `128` (clamped to `0`-`2000`)
|
|
224
|
+
- `sidebar.childrenConcurrency`: `5` (clamped to `1`-`10`)
|
|
225
|
+
|
|
226
|
+
Quota defaults:
|
|
227
|
+
|
|
228
|
+
- `quota.refreshMs`: `300000` (clamped to `>=30000`)
|
|
229
|
+
- `quota.includeOpenAI`: `true`
|
|
230
|
+
- `quota.includeCopilot`: `true`
|
|
231
|
+
- `quota.includeAnthropic`: `true`
|
|
232
|
+
- `quota.providers`: `{}` (per-adapter switches, for example `rightcode.enabled` or `buzz.enabled`)
|
|
233
|
+
- `quota.refreshAccessToken`: `false`
|
|
234
|
+
- `quota.requestTimeoutMs`: `8000` (clamped to `>=1000`)
|
|
235
|
+
|
|
236
|
+
Other defaults:
|
|
237
|
+
|
|
238
|
+
- `toast.durationMs`: `12000` (clamped to `>=1000`)
|
|
239
|
+
- `retentionDays`: `730`
|
|
240
|
+
|
|
241
|
+
### Full example config
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"sidebar": {
|
|
246
|
+
"enabled": true,
|
|
247
|
+
"width": 36,
|
|
248
|
+
"multilineTitle": true,
|
|
249
|
+
"showCost": true,
|
|
250
|
+
"showQuota": true,
|
|
251
|
+
"wrapQuotaLines": true,
|
|
252
|
+
"includeChildren": true,
|
|
253
|
+
"childrenMaxDepth": 6,
|
|
254
|
+
"childrenMaxSessions": 128,
|
|
255
|
+
"childrenConcurrency": 5
|
|
256
|
+
},
|
|
257
|
+
"quota": {
|
|
258
|
+
"refreshMs": 300000,
|
|
259
|
+
"includeOpenAI": true,
|
|
260
|
+
"includeCopilot": true,
|
|
261
|
+
"includeAnthropic": true,
|
|
262
|
+
"providers": {
|
|
263
|
+
"buzz": {
|
|
264
|
+
"enabled": true
|
|
265
|
+
},
|
|
266
|
+
"rightcode": {
|
|
267
|
+
"enabled": true
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
"refreshAccessToken": false,
|
|
271
|
+
"requestTimeoutMs": 8000
|
|
272
|
+
},
|
|
273
|
+
"toast": {
|
|
274
|
+
"durationMs": 12000
|
|
275
|
+
},
|
|
276
|
+
"retentionDays": 730
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Notes
|
|
281
|
+
|
|
282
|
+
- `sidebar.showCost` controls API-cost visibility in sidebar title, `quota_summary` markdown report, and toast message.
|
|
283
|
+
- `quota_summary` follows the same reset compaction rules for short windows in its subscription section (`5h` / `1d` / `Daily` show time, long windows show date, RightCode `Exp` stays date-only).
|
|
284
|
+
- `sidebar.width` is measured in terminal cells. CJK/emoji truncation is best-effort to avoid sidebar overflow.
|
|
285
|
+
- `sidebar.multilineTitle` controls multi-line sidebar layout (default: `true`). Set `false` for compact single-line title.
|
|
286
|
+
- `sidebar.wrapQuotaLines` controls quota line wrapping and continuation indentation (default: `true`).
|
|
287
|
+
- `sidebar.includeChildren` controls whether session-scoped usage/quota includes descendant subagent sessions (default: `true`).
|
|
288
|
+
- `sidebar.childrenMaxDepth` limits how many levels of nested subagents are traversed (default: `6`, clamped 1–32).
|
|
289
|
+
- `sidebar.childrenMaxSessions` caps the total number of descendant sessions aggregated (default: `128`, clamped 0–2000).
|
|
290
|
+
- `sidebar.childrenConcurrency` controls parallel fetches for descendant session messages (default: `5`, clamped 1–10).
|
|
291
|
+
- `output` includes reasoning tokens (`output = tokens.output + tokens.reasoning`). Reasoning is not rendered as a separate line.
|
|
292
|
+
- API cost bills reasoning tokens at the output rate (same as completion tokens).
|
|
293
|
+
- API cost is computed from OpenCode model pricing metadata, not from `message.cost`. This keeps subscription-backed providers such as OpenAI OAuth usable for API-equivalent cost estimation even when OpenCode's measured cost is `0`.
|
|
294
|
+
- When OpenCode exposes a long-context tier like `context_over_200k`, the plugin uses that premium rate for the whole request once `input > 200000`, matching OpenCode's current pricing schema.
|
|
295
|
+
- `quota.providers` is the extensible per-adapter switch map.
|
|
296
|
+
- If API Cost is `$0.00`, it usually means the model/provider has no pricing mapping in OpenCode at the moment, so equivalent API cost cannot be estimated.
|
|
297
|
+
- Usage chunks cache both measured `cost` and computed `apiCost`. `quota_summary` (`/qday`, `/qweek`, `/qmonth`) usually reads those cached aggregates first, but a billing-cache version bump or missing/legacy API-cost data will trigger a rescan and persist refreshed values.
|
|
298
|
+
|
|
299
|
+
### Buzz provider example
|
|
300
|
+
|
|
301
|
+
Buzz matching is based on the provider `baseURL`, similar to RightCode. Any OpenAI-compatible provider that points at `https://buzzai.cc` will be recognized by the Buzz adapter and rendered as a balance-only quota source.
|
|
302
|
+
|
|
303
|
+
Provider options example:
|
|
304
|
+
|
|
305
|
+
```json
|
|
306
|
+
{
|
|
307
|
+
"id": "openai",
|
|
308
|
+
"options": {
|
|
309
|
+
"baseURL": "https://buzzai.cc",
|
|
310
|
+
"apiKey": "sk-..."
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The adapter also tolerates `https://buzzai.cc/v1`, but `https://buzzai.cc` is the recommended example.
|
|
316
|
+
|
|
317
|
+
With that setup, the sidebar/toast quota line will look like:
|
|
318
|
+
|
|
319
|
+
```text
|
|
320
|
+
Buzz Balance CNY 10.17
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Rendering examples
|
|
324
|
+
|
|
325
|
+
These examples show the quota block portion of the sidebar title.
|
|
326
|
+
|
|
327
|
+
### `sidebar.multilineTitle=true`
|
|
328
|
+
|
|
329
|
+
0 providers (no quota data):
|
|
330
|
+
|
|
331
|
+
```text
|
|
332
|
+
(no quota block)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
1 provider, 1 window (fits):
|
|
336
|
+
|
|
337
|
+
```text
|
|
338
|
+
Copilot Monthly 78% Rst 04-01
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
1 provider, multi-window (for example OpenAI 5h + Weekly):
|
|
342
|
+
|
|
343
|
+
```text
|
|
344
|
+
OpenAI
|
|
345
|
+
5h 78% Rst 05:05
|
|
346
|
+
Weekly 73% Rst 03-12
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
1 provider, short window crossing into the next day:
|
|
350
|
+
|
|
351
|
+
```text
|
|
352
|
+
Anthropic
|
|
353
|
+
5h 0% Rst 03-10 01:00
|
|
354
|
+
Weekly 46% Rst 03-15
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
2+ providers (even if each provider is single-window):
|
|
358
|
+
|
|
359
|
+
```text
|
|
360
|
+
OpenAI
|
|
361
|
+
5h 78% Rst 05:05
|
|
362
|
+
Copilot
|
|
363
|
+
Monthly 78% Rst 04-01
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
2+ providers mixed (multi-window + single-window):
|
|
367
|
+
|
|
368
|
+
```text
|
|
369
|
+
OpenAI
|
|
370
|
+
5h 78% Rst 05:05
|
|
371
|
+
Weekly 73% Rst 03-12
|
|
372
|
+
Copilot
|
|
373
|
+
Monthly 78% Rst 04-01
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
2+ providers mixed (window providers + Buzz balance):
|
|
377
|
+
|
|
378
|
+
```text
|
|
379
|
+
OpenAI
|
|
380
|
+
5h 78% Rst 05:05
|
|
381
|
+
Copilot
|
|
382
|
+
Monthly 78% Rst 04-01
|
|
383
|
+
Buzz Balance CNY 10.2
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Balance-style quota:
|
|
387
|
+
|
|
388
|
+
```text
|
|
389
|
+
RC Balance $260
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Buzz balance quota:
|
|
393
|
+
|
|
394
|
+
```text
|
|
395
|
+
Buzz Balance CNY 10.17
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Multi-detail quota (window + balance):
|
|
399
|
+
|
|
400
|
+
```text
|
|
401
|
+
RC
|
|
402
|
+
Daily $88.9/$60 Exp 02-27
|
|
403
|
+
Balance $260
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Provider status / quota (examples):
|
|
407
|
+
|
|
408
|
+
```text
|
|
409
|
+
Anthropic 5h 80%+
|
|
410
|
+
Copilot unavailable
|
|
411
|
+
OpenAI Remaining ?
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### `sidebar.multilineTitle=false`
|
|
415
|
+
|
|
416
|
+
Quota is rendered inline as part of a single-line title:
|
|
417
|
+
|
|
418
|
+
```text
|
|
419
|
+
<base> | Input ... | Output ... | OpenAI 5h 78%+ | Copilot Monthly 78% | ...
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Mixed with Buzz balance:
|
|
423
|
+
|
|
424
|
+
```text
|
|
425
|
+
<base> | Input ... | Output ... | OpenAI 5h 78%+ | Copilot Monthly 78% | Buzz Balance CNY 10.2
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
`quota_summary` also supports an optional `includeChildren` flag (only effective for `period=session`) to override the config per call. For `day`/`week`/`month` periods, children are never merged — each session is counted independently.
|
|
429
|
+
|
|
430
|
+
## Billing cache behavior
|
|
431
|
+
|
|
432
|
+
- Cached per-session usage stores token totals, measured `cost`, computed `apiCost`, provider breakdowns, and the incremental cursor.
|
|
433
|
+
- Session-scoped sidebar aggregation can merge descendant subagents when `sidebar.includeChildren=true` (default). Measured `cost` stays aligned with the root session's OpenCode `message.cost`, while API-equivalent cost still includes descendant usage.
|
|
434
|
+
- Range tools such as `/qday`, `/qweek`, and `/qmonth` do not merge children. They aggregate each session independently across the selected time window.
|
|
435
|
+
- When API-cost logic changes, the plugin bumps an internal billing-cache version so historical range reports are recomputed with the new rules the next time they are queried.
|
|
436
|
+
|
|
437
|
+
## Debug logging
|
|
438
|
+
|
|
439
|
+
Set `OPENCODE_QUOTA_DEBUG=1` to enable debug logging to stderr. This logs:
|
|
440
|
+
|
|
441
|
+
- Chunk I/O operations
|
|
442
|
+
- Auth refresh attempts and failures
|
|
443
|
+
- Session eviction counts
|
|
444
|
+
- Symlink write refusals
|
|
445
|
+
|
|
446
|
+
## Security & privacy notes
|
|
447
|
+
|
|
448
|
+
- The plugin reads OpenCode credentials from `<opencode-data>/auth.json`.
|
|
449
|
+
- If enabled, quota checks call external endpoints:
|
|
450
|
+
- OpenAI Codex: `https://chatgpt.com/backend-api/wham/usage`
|
|
451
|
+
- GitHub Copilot: `https://api.github.com/copilot_internal/user`
|
|
452
|
+
- RightCode: `https://www.right.codes/account/summary`
|
|
453
|
+
- Buzz: `https://buzzai.cc/v1/dashboard/billing/subscription` and `https://buzzai.cc/v1/dashboard/billing/usage`
|
|
454
|
+
- Anthropic: `https://api.anthropic.com/api/oauth/usage`
|
|
455
|
+
- **Screen-sharing warning**: Session titles and toasts surface usage/quota
|
|
456
|
+
information. If you are screen-sharing or recording, consider toggling the
|
|
457
|
+
sidebar display off (`/qtoggle` or `quota_show` tool) to avoid leaking
|
|
458
|
+
subscription details.
|
|
459
|
+
- State is persisted under `<opencode-data>/quota-sidebar.state.json` and
|
|
460
|
+
`<opencode-data>/quota-sidebar-sessions/` (see Storage layout).
|
|
461
|
+
- OpenAI OAuth token refresh is disabled by default; set
|
|
462
|
+
`quota.refreshAccessToken=true` if you want the plugin to refresh access
|
|
463
|
+
tokens when expired.
|
|
464
|
+
- Anthropic quota currently uses a beta/internal-style OAuth usage endpoint and
|
|
465
|
+
request header; response fields may change without notice.
|
|
466
|
+
- State/chunk file writes refuse to write through symlinked targets (best-effort defense-in-depth).
|
|
467
|
+
- The `OPENCODE_QUOTA_DATA_HOME` env var overrides the OpenCode data directory
|
|
468
|
+
path (for testing); do not set this in production.
|
|
469
|
+
- The `OPENCODE_QUOTA_CONFIG_HOME` env var overrides global config directory
|
|
470
|
+
lookup (`<config-home>/opencode`).
|
|
471
|
+
- The `OPENCODE_QUOTA_CONFIG` env var points to an explicit config file and
|
|
472
|
+
applies as the highest-priority override.
|
|
473
|
+
|
|
474
|
+
## Contributing
|
|
475
|
+
|
|
476
|
+
Contributions are welcome — especially new quota provider connectors. See [CONTRIBUTING.md](CONTRIBUTING.md) for a step-by-step guide on adding support for a new provider.
|
|
477
|
+
|
|
478
|
+
## License
|
|
479
|
+
|
|
480
|
+
MIT. See `LICENSE`.
|
package/dist/cost.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AssistantMessage } from '@opencode-ai/sdk';
|
|
2
2
|
export declare const SUBSCRIPTION_API_COST_PROVIDERS: Set<string>;
|
|
3
|
-
export declare function canonicalApiCostProviderID(providerID: string
|
|
3
|
+
export declare function canonicalApiCostProviderID(providerID: string): string;
|
|
4
4
|
export type ModelCostRates = {
|
|
5
5
|
input: number;
|
|
6
6
|
output: number;
|
|
@@ -13,8 +13,7 @@ export type ModelCostRates = {
|
|
|
13
13
|
cacheWrite: number;
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
|
-
export declare function openAIServiceTierFromMessage(message: AssistantMessage): string | undefined;
|
|
17
16
|
export declare function modelCostKey(providerID: string, modelID: string): string;
|
|
18
17
|
export declare function parseModelCostRates(value: unknown): ModelCostRates | undefined;
|
|
19
18
|
export declare function guessModelCostDivisor(rates: ModelCostRates): 1 | 1000000;
|
|
20
|
-
export declare function calcEquivalentApiCostForMessage(message: AssistantMessage, rates: ModelCostRates
|
|
19
|
+
export declare function calcEquivalentApiCostForMessage(message: AssistantMessage, rates: ModelCostRates): number;
|
package/dist/cost.js
CHANGED
|
@@ -5,20 +5,7 @@ function normalizeKnownProviderID(providerID) {
|
|
|
5
5
|
return 'github-copilot';
|
|
6
6
|
return providerID;
|
|
7
7
|
}
|
|
8
|
-
function
|
|
9
|
-
const normalized = npmPackage.trim().toLowerCase();
|
|
10
|
-
if (normalized === '@ai-sdk/openai')
|
|
11
|
-
return 'openai';
|
|
12
|
-
if (normalized === '@ai-sdk/anthropic')
|
|
13
|
-
return 'anthropic';
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
export function canonicalApiCostProviderID(providerID, npmPackage) {
|
|
17
|
-
const byPackage = typeof npmPackage === 'string'
|
|
18
|
-
? canonicalProviderByNpmPackage(npmPackage)
|
|
19
|
-
: undefined;
|
|
20
|
-
if (byPackage)
|
|
21
|
-
return byPackage;
|
|
8
|
+
export function canonicalApiCostProviderID(providerID) {
|
|
22
9
|
const normalized = normalizeKnownProviderID(providerID);
|
|
23
10
|
if (SUBSCRIPTION_API_COST_PROVIDERS.has(normalized))
|
|
24
11
|
return normalized;
|
|
@@ -32,11 +19,6 @@ export function canonicalApiCostProviderID(providerID, npmPackage) {
|
|
|
32
19
|
}
|
|
33
20
|
return normalized;
|
|
34
21
|
}
|
|
35
|
-
export function openAIServiceTierFromMessage(message) {
|
|
36
|
-
const info = message;
|
|
37
|
-
return (info.providerMetadata?.openai?.serviceTier ??
|
|
38
|
-
info.providerMetadata?.openai?.service_tier);
|
|
39
|
-
}
|
|
40
22
|
export function modelCostKey(providerID, modelID) {
|
|
41
23
|
return `${providerID}:${modelID}`;
|
|
42
24
|
}
|
|
@@ -96,20 +78,18 @@ export function guessModelCostDivisor(rates) {
|
|
|
96
78
|
? MODEL_COST_DIVISOR_PER_MILLION
|
|
97
79
|
: MODEL_COST_DIVISOR_PER_TOKEN;
|
|
98
80
|
}
|
|
99
|
-
export function calcEquivalentApiCostForMessage(message, rates
|
|
81
|
+
export function calcEquivalentApiCostForMessage(message, rates) {
|
|
100
82
|
const effectiveRates = message.tokens.input > 200_000 && rates.contextOver200k
|
|
101
83
|
? rates.contextOver200k
|
|
102
84
|
: rates;
|
|
103
|
-
const priorityMultiplier = canonicalProviderID === 'openai' && serviceTier === 'priority' ? 2 : 1;
|
|
104
85
|
// For providers that expose reasoning tokens separately, they are still
|
|
105
86
|
// billed as output/completion tokens (same unit price). Our UI also merges
|
|
106
87
|
// reasoning into the single Output statistic, so API cost should match that.
|
|
107
88
|
const billedOutput = message.tokens.output + message.tokens.reasoning;
|
|
108
|
-
const rawCost =
|
|
89
|
+
const rawCost = message.tokens.input * effectiveRates.input +
|
|
109
90
|
billedOutput * effectiveRates.output +
|
|
110
91
|
message.tokens.cache.read * effectiveRates.cacheRead +
|
|
111
|
-
message.tokens.cache.write * effectiveRates.cacheWrite
|
|
112
|
-
priorityMultiplier;
|
|
92
|
+
message.tokens.cache.write * effectiveRates.cacheWrite;
|
|
113
93
|
const divisor = guessModelCostDivisor(effectiveRates);
|
|
114
94
|
const normalized = rawCost / divisor;
|
|
115
95
|
return Number.isFinite(normalized) && normalized > 0 ? normalized : 0;
|
package/dist/usage_service.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TtlValueCache } from './cache.js';
|
|
2
|
-
import { calcEquivalentApiCostForMessage, canonicalApiCostProviderID, modelCostKey,
|
|
2
|
+
import { calcEquivalentApiCostForMessage, canonicalApiCostProviderID, modelCostKey, parseModelCostRates, SUBSCRIPTION_API_COST_PROVIDERS, } from './cost.js';
|
|
3
3
|
import { dateKeyFromTimestamp, scanSessionsByCreatedRange, updateSessionsInDayChunks, } from './storage.js';
|
|
4
4
|
import { periodStart } from './period.js';
|
|
5
5
|
import { debug, isRecord, mapConcurrent, swallow } from './helpers.js';
|
|
@@ -30,7 +30,7 @@ export function createUsageService(deps) {
|
|
|
30
30
|
return cached;
|
|
31
31
|
const providerClient = deps.client;
|
|
32
32
|
if (!providerClient.provider?.list) {
|
|
33
|
-
return modelCostCache.set({
|
|
33
|
+
return modelCostCache.set({}, 30_000);
|
|
34
34
|
}
|
|
35
35
|
const response = await providerClient.provider
|
|
36
36
|
.list({
|
|
@@ -45,56 +45,11 @@ export function createUsageService(deps) {
|
|
|
45
45
|
Array.isArray(response.data.all)
|
|
46
46
|
? response.data.all
|
|
47
47
|
: [];
|
|
48
|
-
const
|
|
49
|
-
if (!isRecord(provider))
|
|
50
|
-
return acc;
|
|
51
|
-
if (typeof provider.id !== 'string')
|
|
52
|
-
return acc;
|
|
53
|
-
const canonical = canonicalApiCostProviderID(provider.id, typeof provider.npm === 'string' ? provider.npm : undefined);
|
|
54
|
-
if (!SUBSCRIPTION_API_COST_PROVIDERS.has(canonical))
|
|
55
|
-
return acc;
|
|
56
|
-
acc[provider.id] = canonical;
|
|
57
|
-
return acc;
|
|
58
|
-
}, {});
|
|
59
|
-
const modelServiceTiers = all.reduce((acc, provider) => {
|
|
48
|
+
const map = all.reduce((acc, provider) => {
|
|
60
49
|
if (!isRecord(provider))
|
|
61
50
|
return acc;
|
|
62
51
|
const providerID = typeof provider.id === 'string'
|
|
63
|
-
? (
|
|
64
|
-
canonicalApiCostProviderID(provider.id, typeof provider.npm === 'string' ? provider.npm : undefined))
|
|
65
|
-
: undefined;
|
|
66
|
-
if (!providerID)
|
|
67
|
-
return acc;
|
|
68
|
-
if (!SUBSCRIPTION_API_COST_PROVIDERS.has(providerID))
|
|
69
|
-
return acc;
|
|
70
|
-
const models = provider.models;
|
|
71
|
-
if (!isRecord(models))
|
|
72
|
-
return acc;
|
|
73
|
-
for (const [modelKey, modelValue] of Object.entries(models)) {
|
|
74
|
-
if (!isRecord(modelValue))
|
|
75
|
-
continue;
|
|
76
|
-
const options = isRecord(modelValue.options)
|
|
77
|
-
? modelValue.options
|
|
78
|
-
: undefined;
|
|
79
|
-
const serviceTier = typeof options?.serviceTier === 'string'
|
|
80
|
-
? options.serviceTier
|
|
81
|
-
: undefined;
|
|
82
|
-
if (!serviceTier)
|
|
83
|
-
continue;
|
|
84
|
-
const modelID = typeof modelValue.id === 'string' ? modelValue.id : modelKey;
|
|
85
|
-
acc[modelCostKey(providerID, modelID)] = serviceTier;
|
|
86
|
-
if (modelKey !== modelID) {
|
|
87
|
-
acc[modelCostKey(providerID, modelKey)] = serviceTier;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return acc;
|
|
91
|
-
}, {});
|
|
92
|
-
const rates = all.reduce((acc, provider) => {
|
|
93
|
-
if (!isRecord(provider))
|
|
94
|
-
return acc;
|
|
95
|
-
const providerID = typeof provider.id === 'string'
|
|
96
|
-
? (providerAliases[provider.id] ??
|
|
97
|
-
canonicalApiCostProviderID(provider.id, typeof provider.npm === 'string' ? provider.npm : undefined))
|
|
52
|
+
? canonicalApiCostProviderID(provider.id)
|
|
98
53
|
: undefined;
|
|
99
54
|
if (!providerID)
|
|
100
55
|
return acc;
|
|
@@ -117,15 +72,13 @@ export function createUsageService(deps) {
|
|
|
117
72
|
}
|
|
118
73
|
return acc;
|
|
119
74
|
}, {});
|
|
120
|
-
return modelCostCache.set(
|
|
75
|
+
return modelCostCache.set(map, Math.max(30_000, deps.config.quota.refreshMs));
|
|
121
76
|
};
|
|
122
77
|
const calcEquivalentApiCost = (message, modelCostMap) => {
|
|
123
|
-
const providerID =
|
|
124
|
-
? modelCostMap.providerAliases[message.providerID]
|
|
125
|
-
: canonicalApiCostProviderID(message.providerID);
|
|
78
|
+
const providerID = canonicalApiCostProviderID(message.providerID);
|
|
126
79
|
if (!SUBSCRIPTION_API_COST_PROVIDERS.has(providerID))
|
|
127
80
|
return 0;
|
|
128
|
-
const rates = modelCostMap
|
|
81
|
+
const rates = modelCostMap[modelCostKey(providerID, message.modelID)];
|
|
129
82
|
if (!rates) {
|
|
130
83
|
const key = modelCostKey(providerID, message.modelID);
|
|
131
84
|
if (!missingApiCostRateKeys.has(key)) {
|
|
@@ -134,9 +87,7 @@ export function createUsageService(deps) {
|
|
|
134
87
|
}
|
|
135
88
|
return 0;
|
|
136
89
|
}
|
|
137
|
-
|
|
138
|
-
modelCostMap.modelServiceTiers[modelCostKey(providerID, message.modelID)];
|
|
139
|
-
return calcEquivalentApiCostForMessage(message, rates, providerID, serviceTier);
|
|
90
|
+
return calcEquivalentApiCostForMessage(message, rates);
|
|
140
91
|
};
|
|
141
92
|
const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
142
93
|
const decodeTokens = (value) => {
|
|
@@ -192,34 +143,12 @@ export function createUsageService(deps) {
|
|
|
192
143
|
tokens,
|
|
193
144
|
};
|
|
194
145
|
};
|
|
195
|
-
const extractProviderMetadata = (parts) => {
|
|
196
|
-
if (!Array.isArray(parts))
|
|
197
|
-
return undefined;
|
|
198
|
-
for (const part of parts) {
|
|
199
|
-
if (!isRecord(part))
|
|
200
|
-
continue;
|
|
201
|
-
const meta = part.metadata;
|
|
202
|
-
if (isRecord(meta))
|
|
203
|
-
return meta;
|
|
204
|
-
const stateMeta = isRecord(part.state)
|
|
205
|
-
? part.state?.metadata
|
|
206
|
-
: undefined;
|
|
207
|
-
if (isRecord(stateMeta))
|
|
208
|
-
return stateMeta;
|
|
209
|
-
}
|
|
210
|
-
return undefined;
|
|
211
|
-
};
|
|
212
146
|
const decodeMessageEntry = (value) => {
|
|
213
147
|
if (!isRecord(value))
|
|
214
148
|
return undefined;
|
|
215
149
|
const decoded = decodeMessageInfo(value.info);
|
|
216
150
|
if (!decoded)
|
|
217
151
|
return undefined;
|
|
218
|
-
const metadata = extractProviderMetadata(value.parts);
|
|
219
|
-
if (metadata && decoded.role === 'assistant') {
|
|
220
|
-
const msg = decoded;
|
|
221
|
-
msg.providerMetadata = metadata;
|
|
222
|
-
}
|
|
223
152
|
return { info: decoded };
|
|
224
153
|
};
|
|
225
154
|
const decodeMessageEntries = (value) => {
|