@leo000001/opencode-quota-sidebar 1.0.0
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/CHANGELOG.md +70 -0
- package/CONTRIBUTING.md +102 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/SECURITY.md +26 -0
- package/dist/cache.d.ts +6 -0
- package/dist/cache.js +22 -0
- package/dist/cost.d.ts +13 -0
- package/dist/cost.js +76 -0
- package/dist/format.d.ts +21 -0
- package/dist/format.js +426 -0
- package/dist/helpers.d.ts +14 -0
- package/dist/helpers.js +50 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +699 -0
- package/dist/period.d.ts +1 -0
- package/dist/period.js +14 -0
- package/dist/providers/common.d.ts +24 -0
- package/dist/providers/common.js +114 -0
- package/dist/providers/core/anthropic.d.ts +2 -0
- package/dist/providers/core/anthropic.js +46 -0
- package/dist/providers/core/copilot.d.ts +2 -0
- package/dist/providers/core/copilot.js +117 -0
- package/dist/providers/core/openai.d.ts +2 -0
- package/dist/providers/core/openai.js +159 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +14 -0
- package/dist/providers/registry.d.ts +9 -0
- package/dist/providers/registry.js +38 -0
- package/dist/providers/third_party/rightcode.d.ts +2 -0
- package/dist/providers/third_party/rightcode.js +230 -0
- package/dist/providers/types.d.ts +58 -0
- package/dist/providers/types.js +1 -0
- package/dist/quota.d.ts +49 -0
- package/dist/quota.js +116 -0
- package/dist/quota_render.d.ts +5 -0
- package/dist/quota_render.js +85 -0
- package/dist/storage.d.ts +32 -0
- package/dist/storage.js +328 -0
- package/dist/storage_chunks.d.ts +9 -0
- package/dist/storage_chunks.js +147 -0
- package/dist/storage_dates.d.ts +9 -0
- package/dist/storage_dates.js +88 -0
- package/dist/storage_parse.d.ts +4 -0
- package/dist/storage_parse.js +149 -0
- package/dist/storage_paths.d.ts +14 -0
- package/dist/storage_paths.js +31 -0
- package/dist/title.d.ts +8 -0
- package/dist/title.js +38 -0
- package/dist/types.d.ts +116 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +51 -0
- package/dist/usage.js +243 -0
- package/package.json +68 -0
- package/quota-sidebar.config.example.json +25 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Refactor quota provider handling to an adapter registry (`src/providers/*`), so adding a new provider no longer requires editing core dispatch code.
|
|
8
|
+
- Add RightCode provider support (`https://www.right.codes/account/summary`) with baseURL-based matching.
|
|
9
|
+
- RightCode subscription display now uses daily quota logic and ignores tiny plans (`total_quota < 10`); when not matched, falls back to account balance.
|
|
10
|
+
|
|
11
|
+
### UX
|
|
12
|
+
|
|
13
|
+
- Merge Reasoning tokens into Output across sidebar, toast, and markdown report.
|
|
14
|
+
- Sidebar quota windows now support indented multiline layout with compact reset time (`Rst HH:MM` / `Rst MM-DD`).
|
|
15
|
+
- Toast now includes a dedicated `Cost as API` section with per-provider equivalent API cost.
|
|
16
|
+
- Quota rendering now de-duplicates provider snapshots before output (prevents duplicate RightCode lines).
|
|
17
|
+
- Remove `sidebar.maxQuotaProviders` config (unused in current design).
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
- Invalidate stale RightCode cache entries that still use old non-daily window formats (auto-refetch on next render).
|
|
22
|
+
|
|
23
|
+
### Bug Fixes (Critical)
|
|
24
|
+
|
|
25
|
+
- H1: Fix `saveState` empty dirty keys triggering full writeAll on every save.
|
|
26
|
+
- H2: Fix `persistState` dirty-key race condition — delete captured keys instead of clearing the whole set.
|
|
27
|
+
- H3: Fix `pendingAppliedTitle` TTL corruption — detect decorated titles to prevent double-decoration; increase TTL to 15s.
|
|
28
|
+
- H4: Fix `updateAuth` silently dropping token persistence — log and return error snapshot on failure.
|
|
29
|
+
- H5: Add per-session concurrency lock for `applyTitle` to prevent conflicting concurrent updates.
|
|
30
|
+
|
|
31
|
+
### Bug Fixes (Medium)
|
|
32
|
+
|
|
33
|
+
- M1: `refreshTimer` entries are now cleaned up when timers fire.
|
|
34
|
+
- M2: Sessions older than `retentionDays` (default 730 days) are evicted from memory on startup.
|
|
35
|
+
- M3: v1→v2 migration now recovers `createdAt` from v1 data when available.
|
|
36
|
+
- M4: State and chunk file writes are now atomic (write to temp + rename).
|
|
37
|
+
- M5: `flushSave` now flushes current dirty keys even when no timer is pending.
|
|
38
|
+
- M6: `shortNumber` now handles negative, NaN, and Infinity values gracefully.
|
|
39
|
+
- M7: `dateKeysInRange` is capped at 400 iterations to prevent runaway loops.
|
|
40
|
+
- M8: Deduplicated `isRecord`/`asNumber`/`asBoolean` into shared `helpers.ts`.
|
|
41
|
+
- M9: `scanSessionsByCreatedRange` now prefers in-memory state over disk reads.
|
|
42
|
+
- M10: `summarizeRangeUsage` now fetches session messages in parallel (concurrency 5).
|
|
43
|
+
- M11: `saveState` now only iterates sessions belonging to dirty date keys.
|
|
44
|
+
- M12: Removed double timestamp normalization in `dateKeyFromTimestamp`.
|
|
45
|
+
|
|
46
|
+
### Performance
|
|
47
|
+
|
|
48
|
+
- P1: Incremental usage aggregation — tracks last processed message cursor per session.
|
|
49
|
+
- P2: LRU chunk cache (64 entries) for loaded day chunks.
|
|
50
|
+
- P3: `restoreAllVisibleTitles` limited to concurrency 5.
|
|
51
|
+
- P4: `sessionDateMap` dirty tracking integrated with chunk-level dirty keys.
|
|
52
|
+
|
|
53
|
+
### Security
|
|
54
|
+
|
|
55
|
+
- S1: Replaced 15+ silent `.catch(() => undefined)` with debug-mode logging via `swallow()`.
|
|
56
|
+
- S2: Added screen-sharing privacy warning to README.
|
|
57
|
+
- S3: State file writes now refuse to follow symlinks.
|
|
58
|
+
- S4: Renamed `OPENCODE_TEST_HOME` env var to `OPENCODE_QUOTA_DATA_HOME`.
|
|
59
|
+
|
|
60
|
+
### Open-Source Prep
|
|
61
|
+
|
|
62
|
+
- O1: Added `repository`, `homepage`, `bugs`, `author` to package.json; moved SDK deps to `peerDependencies`.
|
|
63
|
+
- O2: Added `*.tsbuildinfo`, `.DS_Store`, `coverage/`, `.env` to `.gitignore`.
|
|
64
|
+
- O3: README config example now includes `sidebar.enabled` and `retentionDays`.
|
|
65
|
+
- O4: Added unit tests for helpers, storage, and usage modules.
|
|
66
|
+
- O5: Main entry now exports consumer types (`QuotaSidebarConfig`, `QuotaSnapshot`, etc.).
|
|
67
|
+
|
|
68
|
+
## 0.1.0
|
|
69
|
+
|
|
70
|
+
- Initial release.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for contributing to opencode-quota-sidebar.
|
|
4
|
+
|
|
5
|
+
The plugin now uses a provider adapter registry, so adding a new provider does not require editing core dispatch logic.
|
|
6
|
+
|
|
7
|
+
## Architecture overview
|
|
8
|
+
|
|
9
|
+
- Provider adapters live in `src/providers/`
|
|
10
|
+
- Built-in adapters live in `src/providers/core/`
|
|
11
|
+
- Third-party/community adapters live in `src/providers/third_party/`
|
|
12
|
+
- `src/providers/registry.ts` resolves which adapter handles a provider
|
|
13
|
+
- `src/quota.ts` provides `createQuotaRuntime()`; runtime methods delegate to resolved adapter and manage auth/cache glue
|
|
14
|
+
- `src/format.ts` renders generic sidebar/report output from `QuotaSnapshot`
|
|
15
|
+
|
|
16
|
+
## Add a new provider
|
|
17
|
+
|
|
18
|
+
### 1) Create an adapter file
|
|
19
|
+
|
|
20
|
+
Add `src/providers/third_party/<your-provider>.ts` and implement `QuotaProviderAdapter`.
|
|
21
|
+
|
|
22
|
+
Minimal shape:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import type { QuotaProviderAdapter } from './types.js'
|
|
26
|
+
|
|
27
|
+
export const myProviderAdapter: QuotaProviderAdapter = {
|
|
28
|
+
id: 'my-provider',
|
|
29
|
+
label: 'My Provider',
|
|
30
|
+
shortLabel: 'MyProv',
|
|
31
|
+
sortOrder: 40,
|
|
32
|
+
matchScore: ({ providerID, providerOptions }) => {
|
|
33
|
+
if (providerID === 'my-provider') return 80
|
|
34
|
+
if (providerOptions?.baseURL === 'https://api.example.com') return 100
|
|
35
|
+
return 0
|
|
36
|
+
},
|
|
37
|
+
isEnabled: (config) =>
|
|
38
|
+
config.quota.providers?.['my-provider']?.enabled ?? true,
|
|
39
|
+
fetch: async ({ providerID, auth, config }) => {
|
|
40
|
+
const checkedAt = Date.now()
|
|
41
|
+
// ... call endpoint, parse payload, return QuotaSnapshot
|
|
42
|
+
return {
|
|
43
|
+
providerID,
|
|
44
|
+
adapterID: 'my-provider',
|
|
45
|
+
label: 'My Provider',
|
|
46
|
+
shortLabel: 'MyProv',
|
|
47
|
+
sortOrder: 40,
|
|
48
|
+
status: 'ok',
|
|
49
|
+
checkedAt,
|
|
50
|
+
remainingPercent: 80,
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2) Register the adapter
|
|
57
|
+
|
|
58
|
+
Edit `src/providers/index.ts` and add `registry.register(myProviderAdapter)`.
|
|
59
|
+
|
|
60
|
+
If your provider has ID variants, implement `normalizeID` in the adapter.
|
|
61
|
+
|
|
62
|
+
### 3) Optional config toggle
|
|
63
|
+
|
|
64
|
+
Add to user config:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"quota": {
|
|
69
|
+
"providers": {
|
|
70
|
+
"my-provider": { "enabled": true }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 4) Add tests
|
|
77
|
+
|
|
78
|
+
At minimum:
|
|
79
|
+
|
|
80
|
+
- adapter selection (`matchScore`)
|
|
81
|
+
- successful response parsing
|
|
82
|
+
- error/unavailable paths
|
|
83
|
+
- format output if using special fields (e.g. `balance`)
|
|
84
|
+
|
|
85
|
+
## QuotaSnapshot rules
|
|
86
|
+
|
|
87
|
+
- Always fill `status` (`ok`, `error`, `unavailable`, `unsupported`)
|
|
88
|
+
- Keep `providerID` as runtime provider identity
|
|
89
|
+
- Set `adapterID`, `label`, `shortLabel`, `sortOrder`
|
|
90
|
+
- Use `windows` for percent-based quota windows
|
|
91
|
+
- Use `balance` for balance-based providers
|
|
92
|
+
- Never throw in adapter `fetch`; return an error snapshot instead
|
|
93
|
+
|
|
94
|
+
## Development workflow
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm install
|
|
98
|
+
npm run build
|
|
99
|
+
npm test
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Both build and tests must pass before submitting a PR.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The opencode-quota-sidebar contributors
|
|
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
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# opencode-quota-sidebar
|
|
2
|
+
|
|
3
|
+
OpenCode plugin: show token usage and subscription quota in the session sidebar title.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Add the package name to `plugin` in your `opencode.json`. OpenCode uses Bun to install it automatically on startup:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"plugin": ["@leo000001/opencode-quota-sidebar"]
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Development (build from source)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
npm run build
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Add the built file to your `opencode.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"plugin": ["file:///ABSOLUTE/PATH/opencode-quota-sidebar/dist/index.js"]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Supported quota providers
|
|
31
|
+
|
|
32
|
+
| Provider | Endpoint | Auth | Status |
|
|
33
|
+
| -------------- | -------------------------------------- | --------------- | -------------------------------------- |
|
|
34
|
+
| OpenAI Codex | `chatgpt.com/backend-api/wham/usage` | OAuth (ChatGPT) | Multi-window (short-term + weekly) |
|
|
35
|
+
| GitHub Copilot | `api.github.com/copilot_internal/user` | OAuth | Monthly quota |
|
|
36
|
+
| RightCode | `www.right.codes/account/summary` | API key | Subscription or balance (by prefix) |
|
|
37
|
+
| Anthropic | — | — | Unsupported (no public quota endpoint) |
|
|
38
|
+
|
|
39
|
+
Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware AI, etc.)? See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- Session title becomes multiline in sidebar:
|
|
44
|
+
- line 1: original session title
|
|
45
|
+
- line 2: Input/Output tokens
|
|
46
|
+
- line 3: Cache Read tokens (only if non-zero)
|
|
47
|
+
- line 4: Cache Write tokens (only if non-zero)
|
|
48
|
+
- line 5: `$X.XX as API cost` (equivalent API billing for subscription-auth providers)
|
|
49
|
+
- quota lines: quota text like `OpenAI 5h 80% Rst 16:20`, with multi-window continuation lines indented (e.g. ` Weekly 70% Rst 03-01`)
|
|
50
|
+
- 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
|
|
51
|
+
- Toast message includes three sections: `Token Usage`, `Cost as API` (per provider), and `Quota`
|
|
52
|
+
- Quota snapshots are de-duplicated before rendering to avoid repeated provider lines
|
|
53
|
+
- Custom tools:
|
|
54
|
+
- `quota_summary` — generate usage report for session/day/week/month (markdown + toast)
|
|
55
|
+
- `quota_show` — toggle sidebar title display on/off (state persists across sessions)
|
|
56
|
+
- Quota connectors:
|
|
57
|
+
- OpenAI Codex OAuth (`/backend-api/wham/usage`)
|
|
58
|
+
- GitHub Copilot OAuth (`/copilot_internal/user`)
|
|
59
|
+
- RightCode API key (`/account/summary`)
|
|
60
|
+
- Anthropic: currently marked unsupported (no public quota endpoint)
|
|
61
|
+
- OpenAI OAuth quota checks auto-refresh expired access token (using refresh token)
|
|
62
|
+
- API key providers still show usage aggregation (quota only applies to subscription providers)
|
|
63
|
+
- Incremental usage aggregation — only processes new messages since last cursor
|
|
64
|
+
- Sidebar token units are adaptive (`k`/`m` with one decimal where applicable)
|
|
65
|
+
|
|
66
|
+
## Storage layout (v2)
|
|
67
|
+
|
|
68
|
+
The plugin stores lightweight global state and date-partitioned session chunks.
|
|
69
|
+
|
|
70
|
+
- Global metadata: `<opencode-data>/quota-sidebar.state.json`
|
|
71
|
+
- `titleEnabled`
|
|
72
|
+
- `sessionDateMap` (sessionID -> `YYYY-MM-DD`)
|
|
73
|
+
- `quotaCache`
|
|
74
|
+
- Session chunks: `<opencode-data>/quota-sidebar-sessions/YYYY/MM/DD.json`
|
|
75
|
+
- per-session title state (`baseTitle`, `lastAppliedTitle`)
|
|
76
|
+
- `createdAt`
|
|
77
|
+
- cached usage summary used by `quota_summary`
|
|
78
|
+
- incremental aggregation cursor
|
|
79
|
+
|
|
80
|
+
Example tree:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
~/.local/share/opencode/
|
|
84
|
+
quota-sidebar.state.json
|
|
85
|
+
quota-sidebar-sessions/
|
|
86
|
+
2026/
|
|
87
|
+
02/
|
|
88
|
+
23.json
|
|
89
|
+
24.json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This replaces the old fixed-entry cap approach. `quota_summary` now scans date chunks
|
|
93
|
+
for day/week/month ranges by session creation date.
|
|
94
|
+
|
|
95
|
+
Sessions older than `retentionDays` (default 730 days / 2 years) are evicted from
|
|
96
|
+
memory on startup. Chunk files remain on disk for historical range scans.
|
|
97
|
+
|
|
98
|
+
## Migration from v1
|
|
99
|
+
|
|
100
|
+
If an old `quota-sidebar.state.json` exists (`version: 1`), the plugin migrates it
|
|
101
|
+
to `version: 2` automatically on load and then persists data in the new chunked layout.
|
|
102
|
+
|
|
103
|
+
On Windows, use forward slashes: `"file:///D:/Lab/opencode-quota-sidebar/dist/index.js"`
|
|
104
|
+
|
|
105
|
+
## Compatibility
|
|
106
|
+
|
|
107
|
+
- Node.js: >= 18 (for `fetch` + `AbortController`)
|
|
108
|
+
- OpenCode: plugin SDK `@opencode-ai/plugin` ^1.2.10
|
|
109
|
+
|
|
110
|
+
## Optional commands
|
|
111
|
+
|
|
112
|
+
You can add these command templates in `opencode.json` so you can run `/qday`, `/qweek`, `/qmonth`, `/qtoggle`:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"command": {
|
|
117
|
+
"qday": {
|
|
118
|
+
"description": "Show today's usage and quota",
|
|
119
|
+
"template": "Call tool quota_summary with period=day and toast=true."
|
|
120
|
+
},
|
|
121
|
+
"qweek": {
|
|
122
|
+
"description": "Show this week's usage and quota",
|
|
123
|
+
"template": "Call tool quota_summary with period=week and toast=true."
|
|
124
|
+
},
|
|
125
|
+
"qmonth": {
|
|
126
|
+
"description": "Show this month's usage and quota",
|
|
127
|
+
"template": "Call tool quota_summary with period=month and toast=true."
|
|
128
|
+
},
|
|
129
|
+
"qtoggle": {
|
|
130
|
+
"description": "Toggle sidebar usage display on/off",
|
|
131
|
+
"template": "Call tool quota_show (no arguments, it toggles)."
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Optional project config
|
|
138
|
+
|
|
139
|
+
Create `quota-sidebar.config.json` under your project root:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"sidebar": {
|
|
144
|
+
"enabled": true,
|
|
145
|
+
"width": 36,
|
|
146
|
+
"showCost": true,
|
|
147
|
+
"showQuota": true
|
|
148
|
+
},
|
|
149
|
+
"quota": {
|
|
150
|
+
"refreshMs": 300000,
|
|
151
|
+
"includeOpenAI": true,
|
|
152
|
+
"includeCopilot": true,
|
|
153
|
+
"includeAnthropic": true,
|
|
154
|
+
"providers": {
|
|
155
|
+
"rightcode": {
|
|
156
|
+
"enabled": true
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"refreshAccessToken": false,
|
|
160
|
+
"requestTimeoutMs": 8000
|
|
161
|
+
},
|
|
162
|
+
"toast": {
|
|
163
|
+
"durationMs": 12000
|
|
164
|
+
},
|
|
165
|
+
"retentionDays": 730
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Notes:
|
|
170
|
+
|
|
171
|
+
- `sidebar.showCost` controls API-cost visibility in sidebar title, `quota_summary` markdown report, and toast message.
|
|
172
|
+
- `output` now includes reasoning tokens. Reasoning is no longer rendered as a separate line.
|
|
173
|
+
- API cost excludes reasoning tokens from output billing (uses `tokens.output` only for output-price multiplication).
|
|
174
|
+
- `quota.providers` is the extensible per-adapter switch map.
|
|
175
|
+
- 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.
|
|
176
|
+
|
|
177
|
+
## Debug logging
|
|
178
|
+
|
|
179
|
+
Set `OPENCODE_QUOTA_DEBUG=1` to enable debug logging to stderr. This logs:
|
|
180
|
+
|
|
181
|
+
- Chunk I/O operations
|
|
182
|
+
- Auth refresh attempts and failures
|
|
183
|
+
- Session eviction counts
|
|
184
|
+
- Symlink write refusals
|
|
185
|
+
|
|
186
|
+
## Independent repository
|
|
187
|
+
|
|
188
|
+
This folder is initialized as its own git repository.
|
|
189
|
+
|
|
190
|
+
## Security & privacy notes
|
|
191
|
+
|
|
192
|
+
- The plugin reads OpenCode credentials from `<opencode-data>/auth.json`.
|
|
193
|
+
- If enabled, quota checks call external endpoints:
|
|
194
|
+
- OpenAI Codex: `https://chatgpt.com/backend-api/wham/usage`
|
|
195
|
+
- GitHub Copilot: `https://api.github.com/copilot_internal/user`
|
|
196
|
+
- RightCode: `https://www.right.codes/account/summary`
|
|
197
|
+
- **Screen-sharing warning**: Session titles and toasts surface usage/quota
|
|
198
|
+
information. If you are screen-sharing or recording, consider toggling the
|
|
199
|
+
sidebar display off (`/qtoggle` or `quota_show` tool) to avoid leaking
|
|
200
|
+
subscription details.
|
|
201
|
+
- State is persisted under `<opencode-data>/quota-sidebar.state.json` and
|
|
202
|
+
`<opencode-data>/quota-sidebar-sessions/` (see Storage layout).
|
|
203
|
+
- OpenAI OAuth token refresh is disabled by default; set
|
|
204
|
+
`quota.refreshAccessToken=true` if you want the plugin to refresh access
|
|
205
|
+
tokens when expired.
|
|
206
|
+
- State file writes refuse to follow symlinks to prevent symlink attacks.
|
|
207
|
+
- The `OPENCODE_QUOTA_DATA_HOME` env var can override the home directory for
|
|
208
|
+
testing; do not set this in production.
|
|
209
|
+
|
|
210
|
+
## Contributing
|
|
211
|
+
|
|
212
|
+
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.
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT. See `LICENSE`.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
If you discover a security issue in this project, please do not open a public issue first.
|
|
6
|
+
|
|
7
|
+
Use one of these channels:
|
|
8
|
+
|
|
9
|
+
1. GitHub Security Advisory (preferred)
|
|
10
|
+
2. Open a private report to repository maintainers
|
|
11
|
+
|
|
12
|
+
Please include:
|
|
13
|
+
|
|
14
|
+
- affected version
|
|
15
|
+
- impact and attack scenario
|
|
16
|
+
- minimal reproduction steps
|
|
17
|
+
- suggested fix (if available)
|
|
18
|
+
|
|
19
|
+
We will acknowledge reports as quickly as possible and provide a remediation timeline.
|
|
20
|
+
|
|
21
|
+
## Security Notes for Contributors
|
|
22
|
+
|
|
23
|
+
- Never commit real tokens, API keys, or auth snapshots.
|
|
24
|
+
- This plugin reads credentials from OpenCode local auth storage; treat all auth data as sensitive.
|
|
25
|
+
- Keep debug logs free of secrets.
|
|
26
|
+
- Prefer fail-closed behavior for writes (already enforced via symlink checks and atomic writes).
|
package/dist/cache.d.ts
ADDED
package/dist/cache.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class TtlValueCache {
|
|
2
|
+
entry;
|
|
3
|
+
get(now = Date.now()) {
|
|
4
|
+
if (!this.entry)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (this.entry.expiresAt <= now) {
|
|
7
|
+
this.entry = undefined;
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
return this.entry.value;
|
|
11
|
+
}
|
|
12
|
+
set(value, ttlMs, now = Date.now()) {
|
|
13
|
+
this.entry = {
|
|
14
|
+
value,
|
|
15
|
+
expiresAt: now + Math.max(0, ttlMs),
|
|
16
|
+
};
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
clear() {
|
|
20
|
+
this.entry = undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/cost.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AssistantMessage } from '@opencode-ai/sdk';
|
|
2
|
+
export declare const SUBSCRIPTION_API_COST_PROVIDERS: Set<string>;
|
|
3
|
+
export declare function canonicalApiCostProviderID(providerID: string): string;
|
|
4
|
+
export type ModelCostRates = {
|
|
5
|
+
input: number;
|
|
6
|
+
output: number;
|
|
7
|
+
cacheRead: number;
|
|
8
|
+
cacheWrite: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function modelCostKey(providerID: string, modelID: string): string;
|
|
11
|
+
export declare function parseModelCostRates(value: unknown): ModelCostRates | undefined;
|
|
12
|
+
export declare function guessModelCostDivisor(rates: ModelCostRates): 1 | 1000000;
|
|
13
|
+
export declare function calcEquivalentApiCostForMessage(message: AssistantMessage, rates: ModelCostRates): number;
|
package/dist/cost.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { asNumber, isRecord } from './helpers.js';
|
|
2
|
+
export const SUBSCRIPTION_API_COST_PROVIDERS = new Set(['openai', 'anthropic']);
|
|
3
|
+
function normalizeKnownProviderID(providerID) {
|
|
4
|
+
if (providerID.startsWith('github-copilot'))
|
|
5
|
+
return 'github-copilot';
|
|
6
|
+
return providerID;
|
|
7
|
+
}
|
|
8
|
+
export function canonicalApiCostProviderID(providerID) {
|
|
9
|
+
const normalized = normalizeKnownProviderID(providerID);
|
|
10
|
+
if (SUBSCRIPTION_API_COST_PROVIDERS.has(normalized))
|
|
11
|
+
return normalized;
|
|
12
|
+
const lowered = providerID.toLowerCase();
|
|
13
|
+
if (lowered.includes('copilot'))
|
|
14
|
+
return 'github-copilot';
|
|
15
|
+
if (lowered.includes('openai') || lowered.endsWith('-oai'))
|
|
16
|
+
return 'openai';
|
|
17
|
+
if (lowered.includes('anthropic') || lowered.includes('claude')) {
|
|
18
|
+
return 'anthropic';
|
|
19
|
+
}
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
export function modelCostKey(providerID, modelID) {
|
|
23
|
+
return `${providerID}:${modelID}`;
|
|
24
|
+
}
|
|
25
|
+
export function parseModelCostRates(value) {
|
|
26
|
+
if (!isRecord(value))
|
|
27
|
+
return undefined;
|
|
28
|
+
const readRate = (input) => {
|
|
29
|
+
if (typeof input === 'number')
|
|
30
|
+
return input;
|
|
31
|
+
if (typeof input === 'string') {
|
|
32
|
+
const parsed = Number(input);
|
|
33
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
34
|
+
}
|
|
35
|
+
if (isRecord(input)) {
|
|
36
|
+
return asNumber(input.usd, asNumber(input.value, asNumber(input.per_1m, asNumber(input.per1m, asNumber(input.per_token, asNumber(input.perToken, 0))))));
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
};
|
|
40
|
+
const cache = isRecord(value.cache) ? value.cache : undefined;
|
|
41
|
+
const input = readRate(value.input ?? value.prompt);
|
|
42
|
+
const output = readRate(value.output ?? value.completion);
|
|
43
|
+
const cacheRead = readRate(value.cache_read ?? cache?.read);
|
|
44
|
+
const cacheWrite = readRate(value.cache_write ?? cache?.write);
|
|
45
|
+
if (input <= 0 && output <= 0 && cacheRead <= 0 && cacheWrite <= 0) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
input,
|
|
50
|
+
output,
|
|
51
|
+
cacheRead,
|
|
52
|
+
cacheWrite,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const MODEL_COST_DIVISOR_PER_TOKEN = 1;
|
|
56
|
+
const MODEL_COST_DIVISOR_PER_MILLION = 1_000_000;
|
|
57
|
+
export function guessModelCostDivisor(rates) {
|
|
58
|
+
// OpenCode provider pricing units can differ:
|
|
59
|
+
// - some providers expose USD per token (e.g. 0.0000025)
|
|
60
|
+
// - others expose USD per 1M tokens (e.g. 2.5)
|
|
61
|
+
// Heuristic: treat values > 0.001 as "per 1M".
|
|
62
|
+
const maxRate = Math.max(rates.input, rates.output, rates.cacheRead, rates.cacheWrite);
|
|
63
|
+
return maxRate > 0.001
|
|
64
|
+
? MODEL_COST_DIVISOR_PER_MILLION
|
|
65
|
+
: MODEL_COST_DIVISOR_PER_TOKEN;
|
|
66
|
+
}
|
|
67
|
+
export function calcEquivalentApiCostForMessage(message, rates) {
|
|
68
|
+
const rawCost = message.tokens.input * rates.input +
|
|
69
|
+
// API cost intentionally excludes reasoning tokens.
|
|
70
|
+
message.tokens.output * rates.output +
|
|
71
|
+
message.tokens.cache.read * rates.cacheRead +
|
|
72
|
+
message.tokens.cache.write * rates.cacheWrite;
|
|
73
|
+
const divisor = guessModelCostDivisor(rates);
|
|
74
|
+
const normalized = rawCost / divisor;
|
|
75
|
+
return Number.isFinite(normalized) && normalized > 0 ? normalized : 0;
|
|
76
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { QuotaSidebarConfig, QuotaSnapshot } from './types.js';
|
|
2
|
+
import type { UsageSummary } from './usage.js';
|
|
3
|
+
/**
|
|
4
|
+
* Render sidebar title with multi-line token breakdown.
|
|
5
|
+
*
|
|
6
|
+
* Layout:
|
|
7
|
+
* Session title
|
|
8
|
+
* Input 18.9k Output 53
|
|
9
|
+
* Cache Read 1.5k (only if read > 0)
|
|
10
|
+
* Cache Write 200 (only if write > 0)
|
|
11
|
+
* $3.81 as API cost (only if showCost=true)
|
|
12
|
+
* OpenAI Remaining 78% (only if quota available)
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderSidebarTitle(baseTitle: string, usage: UsageSummary, quotas: QuotaSnapshot[], config: QuotaSidebarConfig): string;
|
|
15
|
+
export declare function renderMarkdownReport(period: string, usage: UsageSummary, quotas: QuotaSnapshot[], options?: {
|
|
16
|
+
showCost?: boolean;
|
|
17
|
+
}): string;
|
|
18
|
+
export declare function renderToastMessage(period: string, usage: UsageSummary, quotas: QuotaSnapshot[], options?: {
|
|
19
|
+
showCost?: boolean;
|
|
20
|
+
width?: number;
|
|
21
|
+
}): string;
|