@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/CONTRIBUTING.md +102 -0
  3. package/LICENSE +21 -0
  4. package/README.md +216 -0
  5. package/SECURITY.md +26 -0
  6. package/dist/cache.d.ts +6 -0
  7. package/dist/cache.js +22 -0
  8. package/dist/cost.d.ts +13 -0
  9. package/dist/cost.js +76 -0
  10. package/dist/format.d.ts +21 -0
  11. package/dist/format.js +426 -0
  12. package/dist/helpers.d.ts +14 -0
  13. package/dist/helpers.js +50 -0
  14. package/dist/index.d.ts +5 -0
  15. package/dist/index.js +699 -0
  16. package/dist/period.d.ts +1 -0
  17. package/dist/period.js +14 -0
  18. package/dist/providers/common.d.ts +24 -0
  19. package/dist/providers/common.js +114 -0
  20. package/dist/providers/core/anthropic.d.ts +2 -0
  21. package/dist/providers/core/anthropic.js +46 -0
  22. package/dist/providers/core/copilot.d.ts +2 -0
  23. package/dist/providers/core/copilot.js +117 -0
  24. package/dist/providers/core/openai.d.ts +2 -0
  25. package/dist/providers/core/openai.js +159 -0
  26. package/dist/providers/index.d.ts +8 -0
  27. package/dist/providers/index.js +14 -0
  28. package/dist/providers/registry.d.ts +9 -0
  29. package/dist/providers/registry.js +38 -0
  30. package/dist/providers/third_party/rightcode.d.ts +2 -0
  31. package/dist/providers/third_party/rightcode.js +230 -0
  32. package/dist/providers/types.d.ts +58 -0
  33. package/dist/providers/types.js +1 -0
  34. package/dist/quota.d.ts +49 -0
  35. package/dist/quota.js +116 -0
  36. package/dist/quota_render.d.ts +5 -0
  37. package/dist/quota_render.js +85 -0
  38. package/dist/storage.d.ts +32 -0
  39. package/dist/storage.js +328 -0
  40. package/dist/storage_chunks.d.ts +9 -0
  41. package/dist/storage_chunks.js +147 -0
  42. package/dist/storage_dates.d.ts +9 -0
  43. package/dist/storage_dates.js +88 -0
  44. package/dist/storage_parse.d.ts +4 -0
  45. package/dist/storage_parse.js +149 -0
  46. package/dist/storage_paths.d.ts +14 -0
  47. package/dist/storage_paths.js +31 -0
  48. package/dist/title.d.ts +8 -0
  49. package/dist/title.js +38 -0
  50. package/dist/types.d.ts +116 -0
  51. package/dist/types.js +1 -0
  52. package/dist/usage.d.ts +51 -0
  53. package/dist/usage.js +243 -0
  54. package/package.json +68 -0
  55. 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.
@@ -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).
@@ -0,0 +1,6 @@
1
+ export declare class TtlValueCache<T> {
2
+ private entry;
3
+ get(now?: number): T | undefined;
4
+ set(value: T, ttlMs: number, now?: number): T;
5
+ clear(): void;
6
+ }
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
+ }
@@ -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;