@leo000001/opencode-quota-sidebar 1.11.0 → 1.13.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/README.md CHANGED
@@ -18,6 +18,7 @@ Add the package name to `plugin` in your `opencode.json`. OpenCode uses Bun to i
18
18
  ```
19
19
 
20
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.
21
22
 
22
23
  ## Development (build from source)
23
24
 
@@ -38,12 +39,12 @@ On Windows, use forward slashes: `"file:///D:/Lab/opencode-quota-sidebar/dist/in
38
39
 
39
40
  ## Supported quota providers
40
41
 
41
- | Provider | Endpoint | Auth | Status |
42
- | -------------- | -------------------------------------- | --------------- | -------------------------------------- |
43
- | OpenAI Codex | `chatgpt.com/backend-api/wham/usage` | OAuth (ChatGPT) | Multi-window (short-term + weekly) |
44
- | GitHub Copilot | `api.github.com/copilot_internal/user` | OAuth | Monthly quota |
45
- | RightCode | `www.right.codes/account/summary` | API key | Subscription or balance (by prefix) |
46
- | Anthropic | | | Unsupported (no public quota endpoint) |
42
+ | Provider | Endpoint | Auth | Status |
43
+ | -------------- | -------------------------------------- | --------------- | --------------------------------------- |
44
+ | OpenAI Codex | `chatgpt.com/backend-api/wham/usage` | OAuth (ChatGPT) | Multi-window (short-term + weekly) |
45
+ | GitHub Copilot | `api.github.com/copilot_internal/user` | OAuth | Monthly quota |
46
+ | RightCode | `www.right.codes/account/summary` | API key | Subscription or balance (by prefix) |
47
+ | Anthropic | `api.anthropic.com/api/oauth/usage` | OAuth | Multi-window (5h + weekly / plan-based) |
47
48
 
48
49
  Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware AI, etc.)? See [CONTRIBUTING.md](CONTRIBUTING.md).
49
50
 
@@ -55,8 +56,8 @@ Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware
55
56
  - line 3: Cache Read tokens (only if non-zero)
56
57
  - line 4: Cache Write tokens (only if non-zero)
57
58
  - line 5: `$X.XX as API cost` (equivalent API billing for subscription-auth providers)
58
- - quota lines: quota text like `OpenAI 5h 80% Rst 16:20`, with multi-window continuation lines indented (e.g. ` Weekly 70% Rst 03-01`)
59
- - 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
59
+ - 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`
60
+ - 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
60
61
  - 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.
61
62
  - Toast message includes three sections: `Token Usage`, `Cost as API` (per provider), and `Quota`
62
63
  - Quota snapshots are de-duplicated before rendering to avoid repeated provider lines
@@ -67,7 +68,7 @@ Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware
67
68
  - OpenAI Codex OAuth (`/backend-api/wham/usage`)
68
69
  - GitHub Copilot OAuth (`/copilot_internal/user`)
69
70
  - RightCode API key (`/account/summary`)
70
- - Anthropic: currently marked unsupported (no public quota endpoint)
71
+ - Anthropic Claude OAuth (`/api/oauth/usage`, with beta header)
71
72
  - OpenAI OAuth quota checks auto-refresh expired access token (using refresh token)
72
73
  - API key providers still show usage aggregation (quota only applies to subscription providers)
73
74
  - Incremental usage aggregation — only processes new messages since last cursor
@@ -145,7 +146,9 @@ Recommended global config:
145
146
  Optional project overrides:
146
147
 
147
148
  - `<worktree>/quota-sidebar.config.json`
149
+ - `<directory>/quota-sidebar.config.json` (when different from `worktree`)
148
150
  - `<worktree>/.opencode/quota-sidebar.config.json`
151
+ - `<directory>/.opencode/quota-sidebar.config.json` (when different from `worktree`)
149
152
 
150
153
  Optional explicit override:
151
154
 
@@ -158,12 +161,46 @@ Optional config-home override:
158
161
  Resolution order (low -> high):
159
162
 
160
163
  1. Global config (`~/.config/opencode/...`)
161
- 2. Project root config
162
- 3. `.opencode` project config
163
- 4. `OPENCODE_QUOTA_CONFIG`
164
+ 2. `<worktree>/quota-sidebar.config.json`
165
+ 3. `<directory>/quota-sidebar.config.json`
166
+ 4. `<worktree>/.opencode/quota-sidebar.config.json`
167
+ 5. `<directory>/.opencode/quota-sidebar.config.json`
168
+ 6. `OPENCODE_QUOTA_CONFIG`
164
169
 
165
170
  Values are layered; later sources override earlier ones.
166
171
 
172
+ ## Defaults
173
+
174
+ If you do not provide any config file, the plugin uses the built-in defaults below.
175
+
176
+ Sidebar defaults:
177
+
178
+ - `sidebar.enabled`: `true`
179
+ - `sidebar.width`: `36` (clamped to `20`-`60`)
180
+ - `sidebar.multilineTitle`: `true`
181
+ - `sidebar.showCost`: `true`
182
+ - `sidebar.showQuota`: `true`
183
+ - `sidebar.wrapQuotaLines`: `true`
184
+ - `sidebar.includeChildren`: `true`
185
+ - `sidebar.childrenMaxDepth`: `6` (clamped to `1`-`32`)
186
+ - `sidebar.childrenMaxSessions`: `128` (clamped to `0`-`2000`)
187
+ - `sidebar.childrenConcurrency`: `5` (clamped to `1`-`10`)
188
+
189
+ Quota defaults:
190
+
191
+ - `quota.refreshMs`: `300000` (clamped to `>=30000`)
192
+ - `quota.includeOpenAI`: `true`
193
+ - `quota.includeCopilot`: `true`
194
+ - `quota.includeAnthropic`: `true`
195
+ - `quota.providers`: `{}` (per-adapter switches, for example `rightcode.enabled`)
196
+ - `quota.refreshAccessToken`: `false`
197
+ - `quota.requestTimeoutMs`: `8000` (clamped to `>=1000`)
198
+
199
+ Other defaults:
200
+
201
+ - `toast.durationMs`: `12000` (clamped to `>=1000`)
202
+ - `retentionDays`: `730`
203
+
167
204
  Example config:
168
205
 
169
206
  ```json
@@ -203,6 +240,7 @@ Example config:
203
240
  Notes:
204
241
 
205
242
  - `sidebar.showCost` controls API-cost visibility in sidebar title, `quota_summary` markdown report, and toast message.
243
+ - `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).
206
244
  - `sidebar.width` is measured in terminal cells. CJK/emoji truncation is best-effort to avoid sidebar overflow.
207
245
  - `sidebar.multilineTitle` controls multi-line sidebar layout (default: `true`). Set `false` for compact single-line title.
208
246
  - `sidebar.wrapQuotaLines` controls quota line wrapping and continuation indentation (default: `true`).
@@ -215,6 +253,89 @@ Notes:
215
253
  - `quota.providers` is the extensible per-adapter switch map.
216
254
  - 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.
217
255
 
256
+ ## Rendering examples
257
+
258
+ These examples show the quota block portion of the sidebar title.
259
+
260
+ ### `sidebar.multilineTitle=true`
261
+
262
+ 0 providers (no quota data):
263
+
264
+ ```text
265
+ (no quota block)
266
+ ```
267
+
268
+ 1 provider, 1 window (fits):
269
+
270
+ ```text
271
+ Copilot Monthly 78% Rst 04-01
272
+ ```
273
+
274
+ 1 provider, multi-window (for example OpenAI 5h + Weekly):
275
+
276
+ ```text
277
+ OpenAI
278
+ 5h 78% Rst 05:05
279
+ Weekly 73% Rst 03-12
280
+ ```
281
+
282
+ 1 provider, short window crossing into the next day:
283
+
284
+ ```text
285
+ Anthropic
286
+ 5h 0% Rst 03-10 01:00
287
+ Weekly 46% Rst 03-15
288
+ ```
289
+
290
+ 2+ providers (even if each provider is single-window):
291
+
292
+ ```text
293
+ OpenAI
294
+ 5h 78% Rst 05:05
295
+ Copilot
296
+ Monthly 78% Rst 04-01
297
+ ```
298
+
299
+ 2+ providers mixed (multi-window + single-window):
300
+
301
+ ```text
302
+ OpenAI
303
+ 5h 78% Rst 05:05
304
+ Weekly 73% Rst 03-12
305
+ Copilot
306
+ Monthly 78% Rst 04-01
307
+ ```
308
+
309
+ Balance-style quota:
310
+
311
+ ```text
312
+ RC Balance $260
313
+ ```
314
+
315
+ Multi-detail quota (window + balance):
316
+
317
+ ```text
318
+ RC
319
+ Daily $88.9/$60 Exp 02-27
320
+ Balance $260
321
+ ```
322
+
323
+ Provider status / quota (examples):
324
+
325
+ ```text
326
+ Anthropic 5h 80%+
327
+ Copilot unavailable
328
+ OpenAI Remaining ?
329
+ ```
330
+
331
+ ### `sidebar.multilineTitle=false`
332
+
333
+ Quota is rendered inline as part of a single-line title:
334
+
335
+ ```text
336
+ <base> | Input ... | Output ... | OpenAI 5h 78%+ | Copilot Monthly 78% | ...
337
+ ```
338
+
218
339
  `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.
219
340
 
220
341
  ## Debug logging
package/dist/events.d.ts CHANGED
@@ -3,6 +3,9 @@ export declare function createEventDispatcher(handlers: {
3
3
  onSessionCreated: (session: Session) => Promise<void>;
4
4
  onSessionUpdated: (session: Session) => Promise<void>;
5
5
  onSessionDeleted: (session: Session) => Promise<void>;
6
- onMessageRemoved: (sessionID: string) => Promise<void>;
6
+ onMessageRemoved: (info: {
7
+ sessionID: string;
8
+ messageID?: string;
9
+ }) => Promise<void>;
7
10
  onAssistantMessageCompleted: (message: AssistantMessage) => Promise<void>;
8
11
  }): (event: Event) => Promise<void>;
package/dist/events.js CHANGED
@@ -16,7 +16,11 @@ export function createEventDispatcher(handlers) {
16
16
  return;
17
17
  }
18
18
  if (event.type === 'message.removed') {
19
- await handlers.onMessageRemoved(event.properties.sessionID);
19
+ const props = event.properties;
20
+ await handlers.onMessageRemoved({
21
+ sessionID: props.sessionID,
22
+ messageID: typeof props.messageID === 'string' ? props.messageID : undefined,
23
+ });
20
24
  return;
21
25
  }
22
26
  if (event.type !== 'message.updated')
package/dist/format.js CHANGED
@@ -248,6 +248,9 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
248
248
  // Quota lines (one provider per line for stable wrapping)
249
249
  if (config.sidebar.showQuota) {
250
250
  const visibleQuotas = collapseQuotaSnapshots(quotas).filter((q) => ['ok', 'error', 'unsupported', 'unavailable'].includes(q.status));
251
+ // When multiple providers are visible, keep a consistent visual rhythm by
252
+ // always rendering each provider as a header line + indented detail line(s).
253
+ const forceWrappedProviders = visibleQuotas.length > 1;
251
254
  const labelWidth = visibleQuotas.reduce((max, item) => {
252
255
  const label = sanitizeLine(quotaDisplayLabel(item));
253
256
  return Math.max(max, stringCellWidth(label));
@@ -256,6 +259,7 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
256
259
  .flatMap((item) => compactQuotaWide(item, labelWidth, {
257
260
  width,
258
261
  wrapLines: config.sidebar.wrapQuotaLines,
262
+ forceWrapped: forceWrappedProviders,
259
263
  }))
260
264
  .filter((s) => Boolean(s));
261
265
  if (quotaItems.length > 0) {
@@ -290,19 +294,22 @@ function compactQuotaWide(quota, labelWidth = 0, options) {
290
294
  const withLabel = (content) => `${labelPadded} ${content}`;
291
295
  const wrap = options?.wrapLines === true && (options?.width || 0) > 0;
292
296
  const width = options?.width || 0;
297
+ const forceWrapped = options?.forceWrapped === true;
293
298
  /** If inline version overflows, break into label-line + indented detail lines. */
294
299
  const maybeBreak = (inlineText, detailLines) => {
295
300
  const inline = withLabel(inlineText);
301
+ if (forceWrapped)
302
+ return [label, ...detailLines.map((d) => `${detailIndent}${d}`)];
296
303
  if (!wrap || stringCellWidth(inline) <= width)
297
304
  return [inline];
298
305
  return [label, ...detailLines.map((d) => `${detailIndent}${d}`)];
299
306
  };
300
307
  if (quota.status === 'error')
301
- return [withLabel('Remaining ?')];
308
+ return maybeBreak('Remaining ?', ['Remaining ?']);
302
309
  if (quota.status === 'unsupported')
303
- return [withLabel('unsupported')];
310
+ return maybeBreak('unsupported', ['unsupported']);
304
311
  if (quota.status === 'unavailable')
305
- return [withLabel('unavailable')];
312
+ return maybeBreak('unavailable', ['unavailable']);
306
313
  if (quota.status !== 'ok')
307
314
  return [];
308
315
  const balanceText = quota.balance
@@ -318,7 +325,7 @@ function compactQuotaWide(quota, labelWidth = 0, options) {
318
325
  ? [sanitizeLine(win.label), pct]
319
326
  : [sanitizeLine(win.label)]
320
327
  : [pct];
321
- const reset = compactReset(win.resetAt, win.resetLabel);
328
+ const reset = compactReset(win.resetAt, win.resetLabel, win.label);
322
329
  if (reset) {
323
330
  parts.push(`${sanitizeLine(win.resetLabel || 'Rst')} ${reset}`);
324
331
  }
@@ -353,7 +360,12 @@ function compactQuotaWide(quota, labelWidth = 0, options) {
353
360
  const fallbackText = `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`;
354
361
  return maybeBreak(fallbackText, [fallbackText]);
355
362
  }
356
- function compactReset(iso, resetLabel) {
363
+ function isShortResetWindow(label) {
364
+ if (typeof label !== 'string')
365
+ return false;
366
+ return /^\s*(?:\d+\s*[hd]|daily)\b/i.test(label);
367
+ }
368
+ function compactReset(iso, resetLabel, windowLabel) {
357
369
  if (!iso)
358
370
  return undefined;
359
371
  const timestamp = Date.parse(iso);
@@ -371,6 +383,12 @@ function compactReset(iso, resetLabel) {
371
383
  value.getMonth() === now.getMonth() &&
372
384
  value.getDate() === now.getDate();
373
385
  const two = (num) => `${num}`.padStart(2, '0');
386
+ if (isShortResetWindow(windowLabel)) {
387
+ const hhmm = `${two(value.getHours())}:${two(value.getMinutes())}`;
388
+ if (sameDay)
389
+ return hhmm;
390
+ return `${two(value.getMonth() + 1)}-${two(value.getDate())} ${hhmm}`;
391
+ }
374
392
  if (sameDay) {
375
393
  return `${two(value.getHours())}:${two(value.getMinutes())}`;
376
394
  }
@@ -384,6 +402,12 @@ function dateLine(iso) {
384
402
  return iso;
385
403
  return new Date(time).toLocaleString();
386
404
  }
405
+ function reportResetLine(iso, resetLabel, windowLabel) {
406
+ const compact = compactReset(iso, resetLabel, windowLabel);
407
+ if (compact)
408
+ return compact;
409
+ return dateLine(iso);
410
+ }
387
411
  function periodLabel(period) {
388
412
  if (period === 'day')
389
413
  return 'Today';
@@ -455,13 +479,13 @@ export function renderMarkdownReport(period, usage, quotas, options) {
455
479
  return quota.windows.map((win) => {
456
480
  if (win.showPercent === false) {
457
481
  const winLabel = win.label ? ` (${win.label})` : '';
458
- return mdCell(`- ${quota.label}${winLabel}: ${quota.status} | reset ${dateLine(win.resetAt)}`);
482
+ return mdCell(`- ${quota.label}${winLabel}: ${quota.status} | reset ${reportResetLine(win.resetAt, win.resetLabel, win.label)}`);
459
483
  }
460
484
  const remaining = win.remainingPercent === undefined
461
485
  ? '-'
462
486
  : `${win.remainingPercent.toFixed(1)}%`;
463
487
  const winLabel = win.label ? ` (${win.label})` : '';
464
- return mdCell(`- ${quota.label}${winLabel}: ${quota.status} | remaining ${remaining} | reset ${dateLine(win.resetAt)}`);
488
+ return mdCell(`- ${quota.label}${winLabel}: ${quota.status} | remaining ${remaining} | reset ${reportResetLine(win.resetAt, win.resetLabel, win.label)}`);
465
489
  });
466
490
  }
467
491
  if (quota.status === 'ok' && quota.balance) {
@@ -473,7 +497,7 @@ export function renderMarkdownReport(period, usage, quotas, options) {
473
497
  ? '-'
474
498
  : `${quota.remainingPercent.toFixed(1)}%`;
475
499
  return [
476
- mdCell(`- ${quota.label}: ${quota.status} | remaining ${remaining} | reset ${dateLine(quota.resetAt)}${quota.note ? ` | ${quota.note}` : ''}`),
500
+ mdCell(`- ${quota.label}: ${quota.status} | remaining ${remaining} | reset ${reportResetLine(quota.resetAt)}${quota.note ? ` | ${quota.note}` : ''}`),
477
501
  ];
478
502
  });
479
503
  return [
@@ -565,7 +589,7 @@ export function renderToastMessage(period, usage, quotas, options) {
565
589
  const pct = win.remainingPercent === undefined
566
590
  ? '-'
567
591
  : `${win.remainingPercent.toFixed(1)}%`;
568
- const reset = compactReset(win.resetAt, win.resetLabel);
592
+ const reset = compactReset(win.resetAt, win.resetLabel, win.label);
569
593
  const parts = [win.label];
570
594
  if (showPercent)
571
595
  parts.push(pct);
package/dist/index.js CHANGED
@@ -225,9 +225,10 @@ export async function QuotaSidebarPlugin(input) {
225
225
  titleRefresh.schedule(session.parentID, 0);
226
226
  }
227
227
  },
228
- onMessageRemoved: async (sessionID) => {
229
- usageService.markForceRescan(sessionID);
230
- titleRefresh.schedule(sessionID);
228
+ onMessageRemoved: async (info) => {
229
+ usageService.markForceRescan(info.sessionID);
230
+ titleRefresh.schedule(info.sessionID, 0);
231
+ scheduleParentRefreshIfSafe(info.sessionID, state.sessions[info.sessionID]?.parentID);
231
232
  },
232
233
  onAssistantMessageCompleted: async (message) => {
233
234
  usageService.markSessionDirty(message.sessionID);
@@ -1,4 +1,109 @@
1
- import { configuredProviderEnabled } from '../common.js';
1
+ import { swallow } from '../../helpers.js';
2
+ import { asRecord, configuredProviderEnabled, fetchWithTimeout, normalizePercent, toIso, } from '../common.js';
3
+ const ANTHROPIC_OAUTH_USAGE_BETA = 'oauth-2025-04-20';
4
+ function parseAnthropicWindow(value, label) {
5
+ const win = asRecord(value);
6
+ if (!win)
7
+ return undefined;
8
+ const usedPercent = normalizePercent(win.utilization);
9
+ if (usedPercent === undefined)
10
+ return undefined;
11
+ const parsed = {
12
+ label,
13
+ usedPercent,
14
+ remainingPercent: Math.max(0, 100 - usedPercent),
15
+ resetAt: toIso(win.resets_at),
16
+ };
17
+ return parsed;
18
+ }
19
+ async function fetchAnthropicQuota({ providerID, auth, config, }) {
20
+ const checkedAt = Date.now();
21
+ const base = {
22
+ providerID,
23
+ adapterID: 'anthropic',
24
+ label: 'Anthropic',
25
+ shortLabel: 'Anthropic',
26
+ sortOrder: 30,
27
+ };
28
+ if (!auth) {
29
+ return {
30
+ ...base,
31
+ status: 'unavailable',
32
+ checkedAt,
33
+ note: 'auth not found',
34
+ };
35
+ }
36
+ if (auth.type !== 'oauth') {
37
+ return {
38
+ ...base,
39
+ status: 'unsupported',
40
+ checkedAt,
41
+ note: 'api key auth has no quota endpoint',
42
+ };
43
+ }
44
+ if (typeof auth.access !== 'string' || !auth.access) {
45
+ return {
46
+ ...base,
47
+ status: 'unavailable',
48
+ checkedAt,
49
+ note: 'missing oauth access token',
50
+ };
51
+ }
52
+ const response = await fetchWithTimeout('https://api.anthropic.com/api/oauth/usage', {
53
+ method: 'GET',
54
+ headers: {
55
+ Accept: 'application/json',
56
+ Authorization: `Bearer ${auth.access}`,
57
+ 'Content-Type': 'application/json',
58
+ 'User-Agent': 'opencode-quota-sidebar',
59
+ 'anthropic-beta': ANTHROPIC_OAUTH_USAGE_BETA,
60
+ },
61
+ }, config.quota.requestTimeoutMs).catch(swallow('fetchAnthropicQuota:usage'));
62
+ if (!response) {
63
+ return {
64
+ ...base,
65
+ status: 'error',
66
+ checkedAt,
67
+ note: 'network request failed',
68
+ };
69
+ }
70
+ if (!response.ok) {
71
+ return {
72
+ ...base,
73
+ status: 'error',
74
+ checkedAt,
75
+ note: `http ${response.status}`,
76
+ };
77
+ }
78
+ const payload = await response
79
+ .json()
80
+ .catch(swallow('fetchAnthropicQuota:json'));
81
+ const usage = asRecord(payload);
82
+ if (!usage) {
83
+ return {
84
+ ...base,
85
+ status: 'error',
86
+ checkedAt,
87
+ note: 'invalid response',
88
+ };
89
+ }
90
+ const windows = [
91
+ parseAnthropicWindow(usage.five_hour, '5h'),
92
+ parseAnthropicWindow(usage.seven_day, 'Weekly'),
93
+ parseAnthropicWindow(usage.seven_day_sonnet, 'Sonnet 7d'),
94
+ ].filter((window) => Boolean(window));
95
+ const primary = windows[0];
96
+ return {
97
+ ...base,
98
+ status: primary ? 'ok' : 'error',
99
+ checkedAt,
100
+ usedPercent: primary?.usedPercent,
101
+ remainingPercent: primary?.remainingPercent,
102
+ resetAt: primary?.resetAt,
103
+ note: primary ? undefined : 'missing quota fields',
104
+ windows: windows.length > 0 ? windows : undefined,
105
+ };
106
+ }
2
107
  export const anthropicAdapter = {
3
108
  id: 'anthropic',
4
109
  label: 'Anthropic',
@@ -6,41 +111,5 @@ export const anthropicAdapter = {
6
111
  sortOrder: 30,
7
112
  matchScore: ({ providerID }) => (providerID === 'anthropic' ? 80 : 0),
8
113
  isEnabled: (config) => configuredProviderEnabled(config.quota, 'anthropic', config.quota.includeAnthropic),
9
- fetch: async ({ providerID, auth }) => {
10
- const checkedAt = Date.now();
11
- if (!auth) {
12
- return {
13
- providerID,
14
- adapterID: 'anthropic',
15
- label: 'Anthropic',
16
- shortLabel: 'Anthropic',
17
- sortOrder: 30,
18
- status: 'unavailable',
19
- checkedAt,
20
- note: 'auth not found',
21
- };
22
- }
23
- if (auth.type === 'api') {
24
- return {
25
- providerID,
26
- adapterID: 'anthropic',
27
- label: 'Anthropic',
28
- shortLabel: 'Anthropic',
29
- sortOrder: 30,
30
- status: 'unsupported',
31
- checkedAt,
32
- note: 'api key has no public quota endpoint',
33
- };
34
- }
35
- return {
36
- providerID,
37
- adapterID: 'anthropic',
38
- label: 'Anthropic',
39
- shortLabel: 'Anthropic',
40
- sortOrder: 30,
41
- status: 'unsupported',
42
- checkedAt,
43
- note: 'oauth quota endpoint is not publicly documented',
44
- };
45
- },
114
+ fetch: fetchAnthropicQuota,
46
115
  };
@@ -1,5 +1,5 @@
1
1
  import { TtlValueCache } from './cache.js';
2
- import { swallow } from './helpers.js';
2
+ import { isRecord, swallow } from './helpers.js';
3
3
  import { listDefaultQuotaProviderIDs, loadAuthMap, quotaSort } from './quota.js';
4
4
  export function createQuotaService(deps) {
5
5
  const authCache = new TtlValueCache();
@@ -16,26 +16,34 @@ export function createQuotaService(deps) {
16
16
  const cached = providerOptionsCache.get();
17
17
  if (cached)
18
18
  return cached;
19
- const configClient = deps.client;
20
- if (!configClient.config?.providers) {
19
+ const client = deps.client;
20
+ if (!client.config?.providers && !client.provider?.list) {
21
21
  return providerOptionsCache.set({}, 30_000);
22
22
  }
23
- const response = await configClient.config
24
- .providers({
25
- query: { directory: deps.directory },
26
- throwOnError: true,
27
- })
23
+ // Newer runtimes expose config.providers; older clients may only expose
24
+ // provider.list with a slightly different response shape.
25
+ const response = await (client.config?.providers
26
+ ? client.config.providers({
27
+ query: { directory: deps.directory },
28
+ throwOnError: true,
29
+ })
30
+ : client.provider.list({
31
+ query: { directory: deps.directory },
32
+ throwOnError: true,
33
+ }))
28
34
  .catch(swallow('getProviderOptionsMap'));
29
- const data = response &&
30
- typeof response === 'object' &&
31
- 'data' in response &&
32
- response.data &&
33
- typeof response.data === 'object' &&
34
- 'providers' in response.data
35
- ? response.data.providers
35
+ const data = isRecord(response) && isRecord(response.data)
36
+ ? response.data
36
37
  : undefined;
37
- const map = Array.isArray(data)
38
- ? data.reduce((acc, item) => {
38
+ const list = Array.isArray(data?.providers)
39
+ ? data.providers
40
+ : Array.isArray(data?.all)
41
+ ? data.all
42
+ : Array.isArray(data)
43
+ ? data
44
+ : undefined;
45
+ const map = Array.isArray(list)
46
+ ? list.reduce((acc, item) => {
39
47
  if (!item || typeof item !== 'object')
40
48
  return acc;
41
49
  const record = item;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "OpenCode plugin that shows quota and token usage in session titles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",