@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/dist/format.js
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { canonicalProviderID, collapseQuotaSnapshots, displayShortLabel, quotaDisplayLabel, } from './quota_render.js';
|
|
2
|
+
import { stripAnsi } from './title.js';
|
|
3
|
+
/** M6 fix: handle negative, NaN, Infinity gracefully. */
|
|
4
|
+
function shortNumber(value, decimals = 1) {
|
|
5
|
+
if (!Number.isFinite(value) || value < 0)
|
|
6
|
+
return '0';
|
|
7
|
+
if (value >= 1_000_000)
|
|
8
|
+
return `${(value / 1_000_000).toFixed(decimals)}m`;
|
|
9
|
+
if (value >= 1000) {
|
|
10
|
+
const k = value / 1000;
|
|
11
|
+
// Avoid "1000.0k" — promote to "m" when rounding pushes past 999
|
|
12
|
+
if (Number(k.toFixed(decimals)) >= 1000)
|
|
13
|
+
return `${(value / 1_000_000).toFixed(decimals)}m`;
|
|
14
|
+
return `${k.toFixed(decimals)}k`;
|
|
15
|
+
}
|
|
16
|
+
return `${Math.round(value)}`;
|
|
17
|
+
}
|
|
18
|
+
/** Sidebar token display: adaptive short unit (k/m) with one decimal. */
|
|
19
|
+
function sidebarNumber(value) {
|
|
20
|
+
return shortNumber(value, 1);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Truncate `value` to at most `width` visible characters.
|
|
24
|
+
* Keep plain text only (no ANSI) to avoid renderer corruption.
|
|
25
|
+
*/
|
|
26
|
+
function fitLine(value, width) {
|
|
27
|
+
if (width <= 0)
|
|
28
|
+
return '';
|
|
29
|
+
if (value.length > width) {
|
|
30
|
+
return width <= 1 ? value.slice(0, width) : `${value.slice(0, width - 1)}~`;
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function formatApiCostValue(value) {
|
|
35
|
+
const safe = Number.isFinite(value) && value > 0 ? value : 0;
|
|
36
|
+
return `$${safe.toFixed(2)}`;
|
|
37
|
+
}
|
|
38
|
+
function formatApiCostLine(value) {
|
|
39
|
+
return `${formatApiCostValue(value)} as API cost`;
|
|
40
|
+
}
|
|
41
|
+
function alignPairs(pairs, indent = ' ') {
|
|
42
|
+
if (pairs.length === 0)
|
|
43
|
+
return [];
|
|
44
|
+
const labelWidth = Math.max(...pairs.map((pair) => pair.label.length), 0);
|
|
45
|
+
return pairs.map((pair) => {
|
|
46
|
+
if (!pair.label) {
|
|
47
|
+
return `${indent}${' '.repeat(labelWidth)} ${pair.value}`;
|
|
48
|
+
}
|
|
49
|
+
return `${indent}${pair.label.padEnd(labelWidth)} ${pair.value}`;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Render sidebar title with multi-line token breakdown.
|
|
54
|
+
*
|
|
55
|
+
* Layout:
|
|
56
|
+
* Session title
|
|
57
|
+
* Input 18.9k Output 53
|
|
58
|
+
* Cache Read 1.5k (only if read > 0)
|
|
59
|
+
* Cache Write 200 (only if write > 0)
|
|
60
|
+
* $3.81 as API cost (only if showCost=true)
|
|
61
|
+
* OpenAI Remaining 78% (only if quota available)
|
|
62
|
+
*/
|
|
63
|
+
export function renderSidebarTitle(baseTitle, usage, quotas, config) {
|
|
64
|
+
const width = Math.max(8, Math.floor(config.sidebar.width || 36));
|
|
65
|
+
const lines = [];
|
|
66
|
+
const safeBaseTitle = stripAnsi(baseTitle || 'Session').split(/\r?\n/, 1)[0] || 'Session';
|
|
67
|
+
lines.push(fitLine(safeBaseTitle, width));
|
|
68
|
+
lines.push('');
|
|
69
|
+
// Input / Output line
|
|
70
|
+
const io = `Input ${sidebarNumber(usage.input)} Output ${sidebarNumber(usage.output)}`;
|
|
71
|
+
lines.push(fitLine(io, width));
|
|
72
|
+
// Cache lines (provider-compatible across OpenAI/Anthropic/Gemini/Copilot)
|
|
73
|
+
if (usage.cacheRead > 0) {
|
|
74
|
+
lines.push(fitLine(`Cache Read ${sidebarNumber(usage.cacheRead)}`, width));
|
|
75
|
+
}
|
|
76
|
+
if (usage.cacheWrite > 0) {
|
|
77
|
+
lines.push(fitLine(`Cache Write ${sidebarNumber(usage.cacheWrite)}`, width));
|
|
78
|
+
}
|
|
79
|
+
if (config.sidebar.showCost && usage.apiCost > 0) {
|
|
80
|
+
lines.push(fitLine(formatApiCostLine(usage.apiCost), width));
|
|
81
|
+
}
|
|
82
|
+
// Quota lines (one provider per line for stable wrapping)
|
|
83
|
+
if (config.sidebar.showQuota) {
|
|
84
|
+
const visibleQuotas = collapseQuotaSnapshots(quotas).filter((q) => ['ok', 'error', 'unsupported', 'unavailable'].includes(q.status));
|
|
85
|
+
const labelWidth = visibleQuotas.reduce((max, item) => {
|
|
86
|
+
const label = quotaDisplayLabel(item);
|
|
87
|
+
return Math.max(max, label.length);
|
|
88
|
+
}, 0);
|
|
89
|
+
const quotaItems = visibleQuotas
|
|
90
|
+
.flatMap((item) => compactQuotaWide(item, labelWidth))
|
|
91
|
+
.filter((s) => Boolean(s));
|
|
92
|
+
if (quotaItems.length > 0) {
|
|
93
|
+
lines.push('');
|
|
94
|
+
}
|
|
95
|
+
for (const line of quotaItems) {
|
|
96
|
+
lines.push(fitLine(line, width));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Multi-window quota format for sidebar.
|
|
103
|
+
* Single window: "OpenAI 5h 80% Rst 16:20"
|
|
104
|
+
* Multi window: "OpenAI 5h 80% Rst 16:20" + indented next line
|
|
105
|
+
* Copilot: "Copilot Monthly 70% Rst 03-01"
|
|
106
|
+
*/
|
|
107
|
+
function compactQuotaWide(quota, labelWidth = 0) {
|
|
108
|
+
const label = quotaDisplayLabel(quota);
|
|
109
|
+
const labelPadding = ' '.repeat(Math.max(0, labelWidth - label.length));
|
|
110
|
+
const withLabel = (content) => `${label}${labelPadding} ${content}`;
|
|
111
|
+
if (quota.status === 'error')
|
|
112
|
+
return [withLabel('Remaining ?')];
|
|
113
|
+
if (quota.status === 'unsupported')
|
|
114
|
+
return [withLabel('unsupported')];
|
|
115
|
+
if (quota.status === 'unavailable')
|
|
116
|
+
return [withLabel('unavailable')];
|
|
117
|
+
if (quota.status !== 'ok')
|
|
118
|
+
return [];
|
|
119
|
+
const balanceText = quota.balance
|
|
120
|
+
? `Balance ${quota.balance.currency}${quota.balance.amount.toFixed(2)}`
|
|
121
|
+
: undefined;
|
|
122
|
+
const renderWindow = (win) => {
|
|
123
|
+
const showPercent = win.showPercent !== false;
|
|
124
|
+
const pct = win.remainingPercent === undefined
|
|
125
|
+
? '?'
|
|
126
|
+
: `${Math.round(win.remainingPercent)}%`;
|
|
127
|
+
const parts = win.label
|
|
128
|
+
? showPercent
|
|
129
|
+
? [win.label, pct]
|
|
130
|
+
: [win.label]
|
|
131
|
+
: [pct];
|
|
132
|
+
const reset = compactReset(win.resetAt);
|
|
133
|
+
if (reset) {
|
|
134
|
+
parts.push(`${win.resetLabel || 'Rst'} ${reset}`);
|
|
135
|
+
}
|
|
136
|
+
return parts.join(' ');
|
|
137
|
+
};
|
|
138
|
+
// Multi-window rendering
|
|
139
|
+
if (quota.windows && quota.windows.length > 0) {
|
|
140
|
+
const parts = quota.windows.map(renderWindow);
|
|
141
|
+
if (parts.length === 1) {
|
|
142
|
+
const first = withLabel(parts[0]);
|
|
143
|
+
if (balanceText && !parts[0].includes('Balance ')) {
|
|
144
|
+
const indent = ' '.repeat(labelWidth + 1);
|
|
145
|
+
return [first, `${indent}${balanceText}`];
|
|
146
|
+
}
|
|
147
|
+
return [first];
|
|
148
|
+
}
|
|
149
|
+
const indent = ' '.repeat(labelWidth + 1);
|
|
150
|
+
const lines = [
|
|
151
|
+
withLabel(parts[0]),
|
|
152
|
+
...parts.slice(1).map((part) => `${indent}${part}`),
|
|
153
|
+
];
|
|
154
|
+
const alreadyHasBalance = parts.some((part) => part.includes('Balance '));
|
|
155
|
+
if (balanceText && !alreadyHasBalance) {
|
|
156
|
+
lines.push(`${indent}${balanceText}`);
|
|
157
|
+
}
|
|
158
|
+
return lines;
|
|
159
|
+
}
|
|
160
|
+
if (balanceText) {
|
|
161
|
+
return [withLabel(balanceText)];
|
|
162
|
+
}
|
|
163
|
+
// Fallback: single value from top-level remainingPercent
|
|
164
|
+
const percent = quota.remainingPercent === undefined
|
|
165
|
+
? '?'
|
|
166
|
+
: `${Math.round(quota.remainingPercent)}%`;
|
|
167
|
+
const reset = compactReset(quota.resetAt);
|
|
168
|
+
return [withLabel(`Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`)];
|
|
169
|
+
}
|
|
170
|
+
function compactReset(iso) {
|
|
171
|
+
if (!iso)
|
|
172
|
+
return undefined;
|
|
173
|
+
const timestamp = Date.parse(iso);
|
|
174
|
+
if (Number.isNaN(timestamp))
|
|
175
|
+
return undefined;
|
|
176
|
+
const value = new Date(timestamp);
|
|
177
|
+
const now = new Date();
|
|
178
|
+
const sameDay = value.getFullYear() === now.getFullYear() &&
|
|
179
|
+
value.getMonth() === now.getMonth() &&
|
|
180
|
+
value.getDate() === now.getDate();
|
|
181
|
+
const two = (num) => `${num}`.padStart(2, '0');
|
|
182
|
+
if (sameDay) {
|
|
183
|
+
return `${two(value.getHours())}:${two(value.getMinutes())}`;
|
|
184
|
+
}
|
|
185
|
+
return `${two(value.getMonth() + 1)}-${two(value.getDate())}`;
|
|
186
|
+
}
|
|
187
|
+
function dateLine(iso) {
|
|
188
|
+
if (!iso)
|
|
189
|
+
return '-';
|
|
190
|
+
const time = Date.parse(iso);
|
|
191
|
+
if (Number.isNaN(time))
|
|
192
|
+
return iso;
|
|
193
|
+
return new Date(time).toLocaleString();
|
|
194
|
+
}
|
|
195
|
+
function periodLabel(period) {
|
|
196
|
+
if (period === 'day')
|
|
197
|
+
return 'Today';
|
|
198
|
+
if (period === 'week')
|
|
199
|
+
return 'This Week';
|
|
200
|
+
if (period === 'month')
|
|
201
|
+
return 'This Month';
|
|
202
|
+
return 'Current Session';
|
|
203
|
+
}
|
|
204
|
+
export function renderMarkdownReport(period, usage, quotas, options) {
|
|
205
|
+
const showCost = options?.showCost !== false;
|
|
206
|
+
const rightCodeSubscriptionProviderIDs = new Set(collapseQuotaSnapshots(quotas)
|
|
207
|
+
.filter((quota) => quota.adapterID === 'rightcode')
|
|
208
|
+
.filter((quota) => quota.status === 'ok')
|
|
209
|
+
.filter((quota) => Array.isArray(quota.windows) && quota.windows.length)
|
|
210
|
+
.filter((quota) => quota.windows[0].label.startsWith('Daily $'))
|
|
211
|
+
.map((quota) => quota.providerID));
|
|
212
|
+
const measuredCostCell = (providerID, cost) => {
|
|
213
|
+
const canonical = canonicalProviderID(providerID);
|
|
214
|
+
const isSubscription = canonical === 'openai' ||
|
|
215
|
+
canonical === 'github-copilot' ||
|
|
216
|
+
rightCodeSubscriptionProviderIDs.has(providerID);
|
|
217
|
+
if (isSubscription)
|
|
218
|
+
return '-';
|
|
219
|
+
return `$${cost.toFixed(3)}`;
|
|
220
|
+
};
|
|
221
|
+
const isSubscriptionMeasuredProvider = (providerID) => {
|
|
222
|
+
const canonical = canonicalProviderID(providerID);
|
|
223
|
+
return (canonical === 'openai' ||
|
|
224
|
+
canonical === 'github-copilot' ||
|
|
225
|
+
rightCodeSubscriptionProviderIDs.has(providerID));
|
|
226
|
+
};
|
|
227
|
+
const apiCostCell = (providerID, apiCost) => {
|
|
228
|
+
const canonical = canonicalProviderID(providerID);
|
|
229
|
+
if (canonical === 'github-copilot')
|
|
230
|
+
return '-';
|
|
231
|
+
if (!Number.isFinite(apiCost) || apiCost <= 0)
|
|
232
|
+
return '$0.00';
|
|
233
|
+
return `$${apiCost.toFixed(2)}`;
|
|
234
|
+
};
|
|
235
|
+
const measuredCostSummaryValue = () => {
|
|
236
|
+
const providers = Object.values(usage.providers);
|
|
237
|
+
if (providers.length === 0)
|
|
238
|
+
return `$${usage.cost.toFixed(4)}`;
|
|
239
|
+
const hasNonSubscription = providers.some((provider) => !isSubscriptionMeasuredProvider(provider.providerID));
|
|
240
|
+
if (!hasNonSubscription)
|
|
241
|
+
return '-';
|
|
242
|
+
return `$${usage.cost.toFixed(4)}`;
|
|
243
|
+
};
|
|
244
|
+
const apiCostSummaryValue = () => {
|
|
245
|
+
const providers = Object.values(usage.providers);
|
|
246
|
+
if (providers.length === 0)
|
|
247
|
+
return formatApiCostValue(usage.apiCost);
|
|
248
|
+
const hasNonCopilot = providers.some((provider) => canonicalProviderID(provider.providerID) !== 'github-copilot');
|
|
249
|
+
if (!hasNonCopilot)
|
|
250
|
+
return '-';
|
|
251
|
+
return formatApiCostValue(usage.apiCost);
|
|
252
|
+
};
|
|
253
|
+
const providerRows = Object.values(usage.providers)
|
|
254
|
+
.sort((a, b) => b.total - a.total)
|
|
255
|
+
.map((provider) => showCost
|
|
256
|
+
? `| ${provider.providerID} | ${shortNumber(provider.input)} | ${shortNumber(provider.output)} | ${shortNumber(provider.cacheRead + provider.cacheWrite)} | ${shortNumber(provider.total)} | ${measuredCostCell(provider.providerID, provider.cost)} | ${apiCostCell(provider.providerID, provider.apiCost)} |`
|
|
257
|
+
: `| ${provider.providerID} | ${shortNumber(provider.input)} | ${shortNumber(provider.output)} | ${shortNumber(provider.cacheRead + provider.cacheWrite)} | ${shortNumber(provider.total)} |`);
|
|
258
|
+
const quotaLines = collapseQuotaSnapshots(quotas).flatMap((quota) => {
|
|
259
|
+
// Multi-window detail
|
|
260
|
+
if (quota.windows && quota.windows.length > 0 && quota.status === 'ok') {
|
|
261
|
+
return quota.windows.map((win) => {
|
|
262
|
+
if (win.showPercent === false) {
|
|
263
|
+
const winLabel = win.label ? ` (${win.label})` : '';
|
|
264
|
+
return `- ${quota.label}${winLabel}: ${quota.status} | reset ${dateLine(win.resetAt)}`;
|
|
265
|
+
}
|
|
266
|
+
const remaining = win.remainingPercent === undefined
|
|
267
|
+
? '-'
|
|
268
|
+
: `${win.remainingPercent.toFixed(1)}%`;
|
|
269
|
+
const winLabel = win.label ? ` (${win.label})` : '';
|
|
270
|
+
return `- ${quota.label}${winLabel}: ${quota.status} | remaining ${remaining} | reset ${dateLine(win.resetAt)}`;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (quota.status === 'ok' && quota.balance) {
|
|
274
|
+
return [
|
|
275
|
+
`- ${quota.label}: ${quota.status} | balance ${quota.balance.currency}${quota.balance.amount.toFixed(2)}`,
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
const remaining = quota.remainingPercent === undefined
|
|
279
|
+
? '-'
|
|
280
|
+
: `${quota.remainingPercent.toFixed(1)}%`;
|
|
281
|
+
return [
|
|
282
|
+
`- ${quota.label}: ${quota.status} | remaining ${remaining} | reset ${dateLine(quota.resetAt)}${quota.note ? ` | ${quota.note}` : ''}`,
|
|
283
|
+
];
|
|
284
|
+
});
|
|
285
|
+
return [
|
|
286
|
+
`## Quota Report - ${periodLabel(period)}`,
|
|
287
|
+
'',
|
|
288
|
+
`- Sessions: ${usage.sessionCount}`,
|
|
289
|
+
`- Assistant messages: ${usage.assistantMessages}`,
|
|
290
|
+
`- Tokens: input ${usage.input}, output ${usage.output}, cache_read ${usage.cacheRead}, cache_write ${usage.cacheWrite}, total ${usage.total}`,
|
|
291
|
+
...(showCost
|
|
292
|
+
? [
|
|
293
|
+
`- Measured cost: ${measuredCostSummaryValue()}`,
|
|
294
|
+
`- API cost: ${apiCostSummaryValue()}`,
|
|
295
|
+
]
|
|
296
|
+
: []),
|
|
297
|
+
'',
|
|
298
|
+
'### Usage by Provider',
|
|
299
|
+
showCost
|
|
300
|
+
? '| Provider | Input | Output | Cache | Total | Measured Cost | API Cost |'
|
|
301
|
+
: '| Provider | Input | Output | Cache | Total |',
|
|
302
|
+
showCost
|
|
303
|
+
? '|---|---:|---:|---:|---:|---:|---:|'
|
|
304
|
+
: '|---|---:|---:|---:|---:|',
|
|
305
|
+
...(providerRows.length
|
|
306
|
+
? providerRows
|
|
307
|
+
: [showCost ? '| - | - | - | - | - | - | - |' : '| - | - | - | - | - |']),
|
|
308
|
+
'',
|
|
309
|
+
'### Subscription Quota',
|
|
310
|
+
...(quotaLines.length
|
|
311
|
+
? quotaLines
|
|
312
|
+
: ['- no provider quota data available']),
|
|
313
|
+
].join('\n');
|
|
314
|
+
}
|
|
315
|
+
export function renderToastMessage(period, usage, quotas, options) {
|
|
316
|
+
const width = Math.max(24, Math.floor(options?.width || 56));
|
|
317
|
+
const showCost = options?.showCost !== false;
|
|
318
|
+
const lines = [];
|
|
319
|
+
lines.push(fitLine(`${periodLabel(period)} - Total ${shortNumber(usage.total)}`, width));
|
|
320
|
+
lines.push('');
|
|
321
|
+
lines.push(fitLine('Token Usage', width));
|
|
322
|
+
const tokenPairs = [
|
|
323
|
+
{ label: 'Input', value: shortNumber(usage.input) },
|
|
324
|
+
{ label: 'Output', value: shortNumber(usage.output) },
|
|
325
|
+
];
|
|
326
|
+
if (usage.cacheRead > 0) {
|
|
327
|
+
tokenPairs.push({
|
|
328
|
+
label: 'Cache Read',
|
|
329
|
+
value: shortNumber(usage.cacheRead),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (usage.cacheWrite > 0) {
|
|
333
|
+
tokenPairs.push({
|
|
334
|
+
label: 'Cache Write',
|
|
335
|
+
value: shortNumber(usage.cacheWrite),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (showCost) {
|
|
339
|
+
if (usage.apiCost > 0) {
|
|
340
|
+
tokenPairs.push({
|
|
341
|
+
label: 'API Cost',
|
|
342
|
+
value: formatApiCostValue(usage.apiCost),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
lines.push(...alignPairs(tokenPairs).map((line) => fitLine(line, width)));
|
|
347
|
+
if (showCost) {
|
|
348
|
+
const costPairs = Object.values(usage.providers)
|
|
349
|
+
.filter((provider) => canonicalProviderID(provider.providerID) !== 'github-copilot')
|
|
350
|
+
.filter((provider) => provider.apiCost > 0)
|
|
351
|
+
.sort((left, right) => right.apiCost - left.apiCost)
|
|
352
|
+
.map((provider) => ({
|
|
353
|
+
label: displayShortLabel(provider.providerID),
|
|
354
|
+
value: `$${provider.apiCost.toFixed(2)}`,
|
|
355
|
+
}));
|
|
356
|
+
lines.push('');
|
|
357
|
+
lines.push(fitLine('Cost as API', width));
|
|
358
|
+
if (costPairs.length > 0) {
|
|
359
|
+
lines.push(...alignPairs(costPairs).map((line) => fitLine(line, width)));
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
const hasAnyUsage = Object.keys(usage.providers).length > 0;
|
|
363
|
+
lines.push(fitLine(hasAnyUsage ? ' N/A (Copilot)' : ' -', width));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const quotaPairs = collapseQuotaSnapshots(quotas).flatMap((item) => {
|
|
367
|
+
if (item.status === 'ok') {
|
|
368
|
+
if (item.windows && item.windows.length > 0) {
|
|
369
|
+
const pairs = item.windows.map((win, idx) => {
|
|
370
|
+
const showPercent = win.showPercent !== false;
|
|
371
|
+
const pct = win.remainingPercent === undefined
|
|
372
|
+
? '-'
|
|
373
|
+
: `${win.remainingPercent.toFixed(1)}%`;
|
|
374
|
+
const reset = compactReset(win.resetAt);
|
|
375
|
+
const parts = [win.label];
|
|
376
|
+
if (showPercent)
|
|
377
|
+
parts.push(pct);
|
|
378
|
+
if (reset)
|
|
379
|
+
parts.push(`${win.resetLabel || 'Rst'} ${reset}`);
|
|
380
|
+
return {
|
|
381
|
+
label: idx === 0 ? quotaDisplayLabel(item) : '',
|
|
382
|
+
value: parts.filter(Boolean).join(' '),
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
if (item.balance) {
|
|
386
|
+
pairs.push({
|
|
387
|
+
label: '',
|
|
388
|
+
value: `Balance ${item.balance.currency}${item.balance.amount.toFixed(2)}`,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return pairs;
|
|
392
|
+
}
|
|
393
|
+
if (item.balance) {
|
|
394
|
+
return [
|
|
395
|
+
{
|
|
396
|
+
label: quotaDisplayLabel(item),
|
|
397
|
+
value: `Balance ${item.balance.currency}${item.balance.amount.toFixed(2)}`,
|
|
398
|
+
},
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
const percent = item.remainingPercent === undefined
|
|
402
|
+
? '-'
|
|
403
|
+
: `${item.remainingPercent.toFixed(1)}%`;
|
|
404
|
+
const reset = compactReset(item.resetAt);
|
|
405
|
+
return [
|
|
406
|
+
{
|
|
407
|
+
label: quotaDisplayLabel(item),
|
|
408
|
+
value: `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`,
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
if (item.status === 'unsupported') {
|
|
413
|
+
return [{ label: quotaDisplayLabel(item), value: 'unsupported' }];
|
|
414
|
+
}
|
|
415
|
+
if (item.status === 'unavailable') {
|
|
416
|
+
return [{ label: quotaDisplayLabel(item), value: 'unavailable' }];
|
|
417
|
+
}
|
|
418
|
+
return [{ label: quotaDisplayLabel(item), value: 'Remaining ?' }];
|
|
419
|
+
});
|
|
420
|
+
if (quotaPairs.length > 0) {
|
|
421
|
+
lines.push('');
|
|
422
|
+
lines.push(fitLine('Quota', width));
|
|
423
|
+
lines.push(...alignPairs(quotaPairs).map((line) => fitLine(line, width)));
|
|
424
|
+
}
|
|
425
|
+
return lines.join('\n');
|
|
426
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Shared type guards, utilities, and debug logging. */
|
|
2
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
export declare function asNumber(value: unknown, fallback: number): number;
|
|
4
|
+
export declare function asNumber(value: unknown): number | undefined;
|
|
5
|
+
export declare function asBoolean(value: unknown, fallback: boolean): boolean;
|
|
6
|
+
export declare function debug(message: string, ...args: unknown[]): void;
|
|
7
|
+
export declare function debugError(context: string, error: unknown): void;
|
|
8
|
+
/** Returns a `.catch()` handler that logs in debug mode and returns undefined. */
|
|
9
|
+
export declare function swallow(context: string): (error: unknown) => undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Run up to `limit` async tasks concurrently from `items`.
|
|
12
|
+
* Returns results in original order.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Shared type guards, utilities, and debug logging. */
|
|
2
|
+
export function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
export function asNumber(value, fallback) {
|
|
6
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
7
|
+
return fallback;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export function asBoolean(value, fallback) {
|
|
11
|
+
if (typeof value !== 'boolean')
|
|
12
|
+
return fallback;
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
const DEBUG = typeof process !== 'undefined' && process.env.OPENCODE_QUOTA_DEBUG === '1';
|
|
16
|
+
export function debug(message, ...args) {
|
|
17
|
+
if (!DEBUG)
|
|
18
|
+
return;
|
|
19
|
+
console.error(`[quota-sidebar] ${message}`, ...args);
|
|
20
|
+
}
|
|
21
|
+
export function debugError(context, error) {
|
|
22
|
+
if (!DEBUG)
|
|
23
|
+
return;
|
|
24
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
25
|
+
console.error(`[quota-sidebar] ${context}: ${msg}`);
|
|
26
|
+
}
|
|
27
|
+
/** Returns a `.catch()` handler that logs in debug mode and returns undefined. */
|
|
28
|
+
export function swallow(context) {
|
|
29
|
+
return (error) => {
|
|
30
|
+
debugError(context, error);
|
|
31
|
+
return undefined;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Run up to `limit` async tasks concurrently from `items`.
|
|
36
|
+
* Returns results in original order.
|
|
37
|
+
*/
|
|
38
|
+
export async function mapConcurrent(items, limit, fn) {
|
|
39
|
+
const results = new Array(items.length);
|
|
40
|
+
let next = 0;
|
|
41
|
+
async function worker() {
|
|
42
|
+
while (next < items.length) {
|
|
43
|
+
const idx = next++;
|
|
44
|
+
results[idx] = await fn(items[idx], idx);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
48
|
+
await Promise.all(workers);
|
|
49
|
+
return results;
|
|
50
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Hooks, type PluginInput } from '@opencode-ai/plugin';
|
|
2
|
+
export declare function QuotaSidebarPlugin(input: PluginInput): Promise<Hooks>;
|
|
3
|
+
export default QuotaSidebarPlugin;
|
|
4
|
+
export type { QuotaSidebarConfig, QuotaSidebarState, QuotaSnapshot, QuotaStatus, SessionState, CachedSessionUsage, CachedProviderUsage, IncrementalCursor, } from './types.js';
|
|
5
|
+
export type { UsageSummary } from './usage.js';
|