@leo000001/opencode-quota-sidebar 1.13.10 → 2.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/README.md CHANGED
@@ -53,16 +53,18 @@ Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware
53
53
 
54
54
  ## Features
55
55
 
56
- - Session title becomes multiline in sidebar:
57
- - line 1: original session title
58
- - line 2: Input/Output tokens
59
- - line 3: Cache Read tokens (only if non-zero)
60
- - line 4: Cache Write tokens (only if non-zero)
61
- - line 5: `$X.XX as API cost` (equivalent API billing for subscription-auth providers)
62
- - quota lines: quota text like `OpenAI 5h 80% Rst 16:20`; short windows (`5h`, `1d`, `Daily`) show `HH:MM` on same-day resets and `MM-DD HH:MM` when crossing days, while longer windows continue to show `MM-DD`
56
+ - Session title becomes multiline in sidebar:
57
+ - line 1: original session title
58
+ - line 2: Input/Output tokens
59
+ - line 3: Cache Read tokens (only if non-zero)
60
+ - line 4: Cache Write tokens (only if non-zero)
61
+ - next lines: `Cache Coverage` (read/write cache models) and `Cache Read Coverage` (read-only cache models) when enough cache telemetry is available; mixed sessions can show both
62
+ - next line: `$X.XX as API cost` (equivalent API billing for subscription-auth providers)
63
+ - quota lines: quota text like `OpenAI 5h 80% Rst 16:20`; short windows (`5h`, `1d`, `Daily`) show `HH:MM` on same-day resets and `MM-DD HH:MM` when crossing days, while longer windows continue to show `MM-DD`
63
64
  - RightCode daily quota shows `$remaining/$dailyTotal` + expiry (e.g. `RC Daily $105/$60 Exp 02-27`, without trailing percent) and also shows balance on the next indented line when available; `Exp` remains date-only
64
65
  - Session-scoped usage/quota can include descendant subagent sessions (enabled by default via `sidebar.includeChildren=true`). Traversal is bounded by `childrenMaxDepth` (default 6), `childrenMaxSessions` (default 128), and `childrenConcurrency` (default 5); truncation is logged when `OPENCODE_QUOTA_DEBUG=1`. Day/week/month ranges never merge children — only session scope does.
65
- - Toast message includes three sections: `Token Usage`, `Cost as API` (per provider), and `Quota`
66
+ - Toast message includes three sections: `Token Usage`, `Cost as API` (per provider), and `Quota`
67
+ - `quota_summary` markdown / toast also include `Cache Coverage` and `Cache Read Coverage` summary lines when available
66
68
  - Quota snapshots are de-duplicated before rendering to avoid repeated provider lines
67
69
  - Custom tools:
68
70
  - `quota_summary` — generate usage report for session/day/week/month (markdown + toast)
package/dist/cost.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AssistantMessage } from '@opencode-ai/sdk';
2
+ import type { CacheCoverageMode } from './types.js';
2
3
  export declare const SUBSCRIPTION_API_COST_PROVIDERS: Set<string>;
3
4
  export declare function canonicalApiCostProviderID(providerID: string): string;
4
5
  export type ModelCostRates = {
@@ -16,4 +17,5 @@ export type ModelCostRates = {
16
17
  export declare function modelCostKey(providerID: string, modelID: string): string;
17
18
  export declare function parseModelCostRates(value: unknown): ModelCostRates | undefined;
18
19
  export declare function guessModelCostDivisor(rates: ModelCostRates): 1 | 1000000;
20
+ export declare function cacheCoverageModeFromRates(rates: ModelCostRates | undefined): CacheCoverageMode;
19
21
  export declare function calcEquivalentApiCostForMessage(message: AssistantMessage, rates: ModelCostRates): number;
package/dist/cost.js CHANGED
@@ -78,6 +78,15 @@ export function guessModelCostDivisor(rates) {
78
78
  ? MODEL_COST_DIVISOR_PER_MILLION
79
79
  : MODEL_COST_DIVISOR_PER_TOKEN;
80
80
  }
81
+ export function cacheCoverageModeFromRates(rates) {
82
+ if (!rates)
83
+ return 'none';
84
+ if (rates.cacheWrite > 0)
85
+ return 'read-write';
86
+ if (rates.cacheRead > 0)
87
+ return 'read-only';
88
+ return 'none';
89
+ }
81
90
  export function calcEquivalentApiCostForMessage(message, rates) {
82
91
  const effectiveRates = message.tokens.input > 200_000 && rates.contextOver200k
83
92
  ? rates.contextOver200k
package/dist/format.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { QuotaSidebarConfig, QuotaSnapshot } from './types.js';
2
- import type { UsageSummary } from './usage.js';
2
+ import { type UsageSummary } from './usage.js';
3
3
  /**
4
4
  * Render sidebar title with multi-line token breakdown.
5
5
  *
package/dist/format.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { getCacheCoverageMetrics } from './usage.js';
1
2
  import { canonicalProviderID, collapseQuotaSnapshots, displayShortLabel, quotaDisplayLabel, } from './quota_render.js';
2
3
  import { stripAnsi } from './title.js';
3
4
  /** M6 fix: handle negative, NaN, Infinity gracefully. */
@@ -147,6 +148,11 @@ function formatApiCostValue(value) {
147
148
  function formatApiCostLine(value) {
148
149
  return `${formatApiCostValue(value)} as API cost`;
149
150
  }
151
+ function formatPercent(value, decimals = 1) {
152
+ const safe = Number.isFinite(value) && value >= 0 ? value : 0;
153
+ const pct = (safe * 100).toFixed(decimals);
154
+ return `${pct.replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1')}%`;
155
+ }
150
156
  function alignPairs(pairs, indent = ' ') {
151
157
  if (pairs.length === 0)
152
158
  return [];
@@ -191,6 +197,7 @@ function compactQuotaInline(quota) {
191
197
  function renderSingleLineTitle(baseTitle, usage, quotas, config, width) {
192
198
  const baseBudget = Math.min(16, Math.max(8, Math.floor(width * 0.35)));
193
199
  const base = fitLine(baseTitle, baseBudget);
200
+ const cacheMetrics = getCacheCoverageMetrics(usage);
194
201
  const segments = [
195
202
  `Input ${sidebarNumber(usage.input)} Output ${sidebarNumber(usage.output)}`,
196
203
  ];
@@ -200,6 +207,12 @@ function renderSingleLineTitle(baseTitle, usage, quotas, config, width) {
200
207
  if (usage.cacheWrite > 0) {
201
208
  segments.push(`Cache Write ${sidebarNumber(usage.cacheWrite)}`);
202
209
  }
210
+ if (cacheMetrics.cacheCoverage !== undefined) {
211
+ segments.push(`Cache Coverage ${formatPercent(cacheMetrics.cacheCoverage, 0)}`);
212
+ }
213
+ if (cacheMetrics.cacheReadCoverage !== undefined) {
214
+ segments.push(`Cache Read Coverage ${formatPercent(cacheMetrics.cacheReadCoverage, 0)}`);
215
+ }
203
216
  if (config.sidebar.showCost && usage.apiCost > 0) {
204
217
  segments.push(formatApiCostLine(usage.apiCost));
205
218
  }
@@ -229,6 +242,7 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
229
242
  if (config.sidebar.multilineTitle !== true) {
230
243
  return renderSingleLineTitle(safeBaseTitle, usage, quotas, config, width);
231
244
  }
245
+ const cacheMetrics = getCacheCoverageMetrics(usage);
232
246
  const lines = [];
233
247
  lines.push(fitLine(safeBaseTitle, width));
234
248
  lines.push('');
@@ -242,6 +256,12 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
242
256
  if (usage.cacheWrite > 0) {
243
257
  lines.push(fitLine(`Cache Write ${sidebarNumber(usage.cacheWrite)}`, width));
244
258
  }
259
+ if (cacheMetrics.cacheCoverage !== undefined) {
260
+ lines.push(fitLine(`Cache Coverage ${formatPercent(cacheMetrics.cacheCoverage, 0)}`, width));
261
+ }
262
+ if (cacheMetrics.cacheReadCoverage !== undefined) {
263
+ lines.push(fitLine(`Cache Read Coverage ${formatPercent(cacheMetrics.cacheReadCoverage, 0)}`, width));
264
+ }
245
265
  if (config.sidebar.showCost && usage.apiCost > 0) {
246
266
  lines.push(fitLine(formatApiCostLine(usage.apiCost), width));
247
267
  }
@@ -419,6 +439,7 @@ function periodLabel(period) {
419
439
  }
420
440
  export function renderMarkdownReport(period, usage, quotas, options) {
421
441
  const showCost = options?.showCost !== false;
442
+ const cacheMetrics = getCacheCoverageMetrics(usage);
422
443
  const mdCell = (value) => sanitizeLine(value).replace(/\|/g, '\\|');
423
444
  const rightCodeSubscriptionProviderIDs = new Set(collapseQuotaSnapshots(quotas)
424
445
  .filter((quota) => quota.adapterID === 'rightcode')
@@ -506,6 +527,14 @@ export function renderMarkdownReport(period, usage, quotas, options) {
506
527
  `- Sessions: ${usage.sessionCount}`,
507
528
  `- Assistant messages: ${usage.assistantMessages}`,
508
529
  `- Tokens: input ${usage.input}, output ${usage.output}, cache_read ${usage.cacheRead}, cache_write ${usage.cacheWrite}, total ${usage.total}`,
530
+ ...(cacheMetrics.cacheCoverage !== undefined
531
+ ? [`- Cache Coverage: ${formatPercent(cacheMetrics.cacheCoverage, 1)}`]
532
+ : []),
533
+ ...(cacheMetrics.cacheReadCoverage !== undefined
534
+ ? [
535
+ `- Cache Read Coverage: ${formatPercent(cacheMetrics.cacheReadCoverage, 1)}`,
536
+ ]
537
+ : []),
509
538
  ...(showCost
510
539
  ? [
511
540
  `- Measured cost: ${measuredCostSummaryValue()}`,
@@ -533,6 +562,7 @@ export function renderMarkdownReport(period, usage, quotas, options) {
533
562
  export function renderToastMessage(period, usage, quotas, options) {
534
563
  const width = Math.max(24, Math.floor(options?.width || 56));
535
564
  const showCost = options?.showCost !== false;
565
+ const cacheMetrics = getCacheCoverageMetrics(usage);
536
566
  const lines = [];
537
567
  lines.push(fitLine(`${periodLabel(period)} - Total ${shortNumber(usage.total)}`, width));
538
568
  lines.push('');
@@ -553,6 +583,18 @@ export function renderToastMessage(period, usage, quotas, options) {
553
583
  value: shortNumber(usage.cacheWrite),
554
584
  });
555
585
  }
586
+ if (cacheMetrics.cacheCoverage !== undefined) {
587
+ tokenPairs.push({
588
+ label: 'Cache Coverage',
589
+ value: formatPercent(cacheMetrics.cacheCoverage, 1),
590
+ });
591
+ }
592
+ if (cacheMetrics.cacheReadCoverage !== undefined) {
593
+ tokenPairs.push({
594
+ label: 'Cache Read Coverage',
595
+ value: formatPercent(cacheMetrics.cacheReadCoverage, 1),
596
+ });
597
+ }
556
598
  if (showCost) {
557
599
  if (usage.apiCost > 0) {
558
600
  tokenPairs.push({
@@ -28,6 +28,38 @@ function parseProviderUsage(value) {
28
28
  assistantMessages: asNumber(value.assistantMessages, 0),
29
29
  };
30
30
  }
31
+ function parseCacheUsageBucket(value) {
32
+ if (!isRecord(value))
33
+ return undefined;
34
+ return {
35
+ input: asNumber(value.input, 0),
36
+ cacheRead: asNumber(value.cacheRead, 0),
37
+ cacheWrite: asNumber(value.cacheWrite, 0),
38
+ assistantMessages: asNumber(value.assistantMessages, 0),
39
+ };
40
+ }
41
+ function parseCacheUsageBuckets(value) {
42
+ if (!isRecord(value))
43
+ return undefined;
44
+ const readOnly = parseCacheUsageBucket(value.readOnly);
45
+ const readWrite = parseCacheUsageBucket(value.readWrite);
46
+ if (!readOnly && !readWrite)
47
+ return undefined;
48
+ return {
49
+ readOnly: readOnly || {
50
+ input: 0,
51
+ cacheRead: 0,
52
+ cacheWrite: 0,
53
+ assistantMessages: 0,
54
+ },
55
+ readWrite: readWrite || {
56
+ input: 0,
57
+ cacheRead: 0,
58
+ cacheWrite: 0,
59
+ assistantMessages: 0,
60
+ },
61
+ };
62
+ }
31
63
  function parseCachedUsage(value) {
32
64
  if (!isRecord(value))
33
65
  return undefined;
@@ -50,6 +82,7 @@ function parseCachedUsage(value) {
50
82
  cost: asNumber(value.cost, 0),
51
83
  apiCost: asNumber(value.apiCost, 0),
52
84
  assistantMessages: asNumber(value.assistantMessages, 0),
85
+ cacheBuckets: parseCacheUsageBuckets(value.cacheBuckets),
53
86
  providers,
54
87
  };
55
88
  }
package/dist/title.js CHANGED
@@ -10,6 +10,8 @@ function isStrongDecoratedDetail(line) {
10
10
  return true;
11
11
  if (/^Cache\s+(Read|Write)\s+\S+/.test(line))
12
12
  return true;
13
+ if (/^Cache(?:\s+Read)?\s+Coverage\s+\S+/.test(line))
14
+ return true;
13
15
  if (/^\$\S+\s+as API cost\b/.test(line))
14
16
  return true;
15
17
  // Single-line compact mode compatibility.
package/dist/types.d.ts CHANGED
@@ -36,6 +36,17 @@ export type SessionTitleState = {
36
36
  baseTitle: string;
37
37
  lastAppliedTitle?: string;
38
38
  };
39
+ export type CacheCoverageMode = 'none' | 'read-only' | 'read-write';
40
+ export type CacheUsageBucket = {
41
+ input: number;
42
+ cacheRead: number;
43
+ cacheWrite: number;
44
+ assistantMessages: number;
45
+ };
46
+ export type CacheUsageBuckets = {
47
+ readOnly: CacheUsageBucket;
48
+ readWrite: CacheUsageBucket;
49
+ };
39
50
  export type CachedProviderUsage = {
40
51
  input: number;
41
52
  output: number;
@@ -61,6 +72,8 @@ export type CachedSessionUsage = {
61
72
  /** Equivalent API billing cost (USD) computed from model pricing. */
62
73
  apiCost: number;
63
74
  assistantMessages: number;
75
+ /** Cache coverage buckets grouped by model cache behavior. */
76
+ cacheBuckets?: CacheUsageBuckets;
64
77
  providers: Record<string, CachedProviderUsage>;
65
78
  };
66
79
  /** Tracks incremental aggregation cursor for a session (P1). */
package/dist/usage.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { AssistantMessage, Message } from '@opencode-ai/sdk';
2
- import type { CachedSessionUsage, IncrementalCursor } from './types.js';
3
- export declare const USAGE_BILLING_CACHE_VERSION = 2;
2
+ import type { CacheCoverageMode, CacheUsageBuckets, CachedSessionUsage, IncrementalCursor } from './types.js';
3
+ export declare const USAGE_BILLING_CACHE_VERSION = 3;
4
4
  export type ProviderUsage = {
5
5
  providerID: string;
6
6
  input: number;
@@ -26,11 +26,18 @@ export type UsageSummary = {
26
26
  apiCost: number;
27
27
  assistantMessages: number;
28
28
  sessionCount: number;
29
+ cacheBuckets?: CacheUsageBuckets;
29
30
  providers: Record<string, ProviderUsage>;
30
31
  };
31
32
  export type UsageOptions = {
32
33
  /** Equivalent API cost calculator for the message. */
33
34
  calcApiCost?: (message: AssistantMessage) => number;
35
+ /** Cache-behavior classifier for the message model/provider. */
36
+ classifyCacheMode?: (message: AssistantMessage) => CacheCoverageMode;
37
+ };
38
+ export declare function getCacheCoverageMetrics(usage: Pick<UsageSummary, 'input' | 'cacheRead' | 'cacheWrite' | 'assistantMessages' | 'cacheBuckets'>): {
39
+ cacheCoverage: number | undefined;
40
+ cacheReadCoverage: number | undefined;
34
41
  };
35
42
  export declare function emptyUsageSummary(): UsageSummary;
36
43
  export declare function summarizeMessages(entries: Array<{
package/dist/usage.js CHANGED
@@ -1,4 +1,94 @@
1
- export const USAGE_BILLING_CACHE_VERSION = 2;
1
+ export const USAGE_BILLING_CACHE_VERSION = 3;
2
+ function emptyCacheUsageBucket() {
3
+ return {
4
+ input: 0,
5
+ cacheRead: 0,
6
+ cacheWrite: 0,
7
+ assistantMessages: 0,
8
+ };
9
+ }
10
+ function emptyCacheUsageBuckets() {
11
+ return {
12
+ readOnly: emptyCacheUsageBucket(),
13
+ readWrite: emptyCacheUsageBucket(),
14
+ };
15
+ }
16
+ function cloneCacheUsageBucket(bucket) {
17
+ return {
18
+ input: bucket?.input ?? 0,
19
+ cacheRead: bucket?.cacheRead ?? 0,
20
+ cacheWrite: bucket?.cacheWrite ?? 0,
21
+ assistantMessages: bucket?.assistantMessages ?? 0,
22
+ };
23
+ }
24
+ function cloneCacheUsageBuckets(buckets) {
25
+ if (!buckets)
26
+ return undefined;
27
+ return {
28
+ readOnly: cloneCacheUsageBucket(buckets?.readOnly),
29
+ readWrite: cloneCacheUsageBucket(buckets?.readWrite),
30
+ };
31
+ }
32
+ function mergeCacheUsageBucket(target, source) {
33
+ if (!source)
34
+ return target;
35
+ target.input += source.input;
36
+ target.cacheRead += source.cacheRead;
37
+ target.cacheWrite += source.cacheWrite;
38
+ target.assistantMessages += source.assistantMessages;
39
+ return target;
40
+ }
41
+ function addMessageCacheUsage(target, message) {
42
+ target.input += message.tokens.input;
43
+ target.cacheRead += message.tokens.cache.read;
44
+ target.cacheWrite += message.tokens.cache.write;
45
+ target.assistantMessages += 1;
46
+ }
47
+ function fallbackCacheUsageBuckets(usage) {
48
+ if (usage.cacheWrite > 0) {
49
+ return {
50
+ readOnly: emptyCacheUsageBucket(),
51
+ readWrite: {
52
+ input: usage.input,
53
+ cacheRead: usage.cacheRead,
54
+ cacheWrite: usage.cacheWrite,
55
+ assistantMessages: usage.assistantMessages,
56
+ },
57
+ };
58
+ }
59
+ if (usage.cacheRead > 0) {
60
+ return {
61
+ readOnly: {
62
+ input: usage.input,
63
+ cacheRead: usage.cacheRead,
64
+ cacheWrite: 0,
65
+ assistantMessages: usage.assistantMessages,
66
+ },
67
+ readWrite: emptyCacheUsageBucket(),
68
+ };
69
+ }
70
+ return undefined;
71
+ }
72
+ function resolvedCacheUsageBuckets(usage) {
73
+ return (cloneCacheUsageBuckets(usage.cacheBuckets || fallbackCacheUsageBuckets(usage)) ||
74
+ emptyCacheUsageBuckets());
75
+ }
76
+ export function getCacheCoverageMetrics(usage) {
77
+ const buckets = resolvedCacheUsageBuckets(usage);
78
+ const readWritePromptSurface = buckets.readWrite.input +
79
+ buckets.readWrite.cacheRead +
80
+ buckets.readWrite.cacheWrite;
81
+ const readOnlyPromptSurface = buckets.readOnly.input + buckets.readOnly.cacheRead;
82
+ return {
83
+ cacheCoverage: readWritePromptSurface > 0
84
+ ? (buckets.readWrite.cacheRead + buckets.readWrite.cacheWrite) /
85
+ readWritePromptSurface
86
+ : undefined,
87
+ cacheReadCoverage: readOnlyPromptSurface > 0
88
+ ? buckets.readOnly.cacheRead / readOnlyPromptSurface
89
+ : undefined,
90
+ };
91
+ }
2
92
  export function emptyUsageSummary() {
3
93
  return {
4
94
  input: 0,
@@ -11,6 +101,7 @@ export function emptyUsageSummary() {
11
101
  apiCost: 0,
12
102
  assistantMessages: 0,
13
103
  sessionCount: 0,
104
+ cacheBuckets: emptyCacheUsageBuckets(),
14
105
  providers: {},
15
106
  };
16
107
  }
@@ -69,6 +160,15 @@ function addMessageUsage(target, message, options) {
69
160
  provider.apiCost += apiCost;
70
161
  provider.assistantMessages += 1;
71
162
  target.providers[message.providerID] = provider;
163
+ const cacheMode = options?.classifyCacheMode?.(message) || 'none';
164
+ if (cacheMode === 'read-only') {
165
+ const buckets = (target.cacheBuckets ||= emptyCacheUsageBuckets());
166
+ addMessageCacheUsage(buckets.readOnly, message);
167
+ }
168
+ else if (cacheMode === 'read-write') {
169
+ const buckets = (target.cacheBuckets ||= emptyCacheUsageBuckets());
170
+ addMessageCacheUsage(buckets.readWrite, message);
171
+ }
72
172
  }
73
173
  export function summarizeMessages(entries, startAt = 0, sessionCount = 1, options) {
74
174
  const summary = emptyUsageSummary();
@@ -276,6 +376,12 @@ export function mergeUsage(target, source, options) {
276
376
  target.apiCost += source.apiCost;
277
377
  target.assistantMessages += source.assistantMessages;
278
378
  target.sessionCount += source.sessionCount;
379
+ const targetBuckets = (target.cacheBuckets ||= emptyCacheUsageBuckets());
380
+ const sourceBuckets = source.cacheBuckets;
381
+ if (sourceBuckets) {
382
+ mergeCacheUsageBucket(targetBuckets.readOnly, sourceBuckets.readOnly);
383
+ mergeCacheUsageBucket(targetBuckets.readWrite, sourceBuckets.readWrite);
384
+ }
279
385
  for (const provider of Object.values(source.providers)) {
280
386
  const existing = target.providers[provider.providerID] ||
281
387
  emptyProviderUsage(provider.providerID);
@@ -321,12 +427,14 @@ export function toCachedSessionUsage(summary) {
321
427
  cost: summary.cost,
322
428
  apiCost: summary.apiCost,
323
429
  assistantMessages: summary.assistantMessages,
430
+ cacheBuckets: cloneCacheUsageBuckets(summary.cacheBuckets),
324
431
  providers,
325
432
  };
326
433
  }
327
434
  export function fromCachedSessionUsage(cached, sessionCount = 1) {
328
435
  // Merge cached reasoning into output for a single output metric.
329
436
  const mergedOutputValue = cached.output + cached.reasoning;
437
+ const cacheBuckets = cloneCacheUsageBuckets(cached.cacheBuckets);
330
438
  return {
331
439
  input: cached.input,
332
440
  output: mergedOutputValue,
@@ -338,6 +446,7 @@ export function fromCachedSessionUsage(cached, sessionCount = 1) {
338
446
  apiCost: cached.apiCost || 0,
339
447
  assistantMessages: cached.assistantMessages,
340
448
  sessionCount,
449
+ cacheBuckets,
341
450
  providers: Object.entries(cached.providers).reduce((acc, [providerID, provider]) => {
342
451
  acc[providerID] = {
343
452
  providerID,
@@ -1,5 +1,5 @@
1
1
  import { TtlValueCache } from './cache.js';
2
- import { calcEquivalentApiCostForMessage, canonicalApiCostProviderID, modelCostKey, parseModelCostRates, SUBSCRIPTION_API_COST_PROVIDERS, } from './cost.js';
2
+ import { cacheCoverageModeFromRates, calcEquivalentApiCostForMessage, canonicalApiCostProviderID, modelCostKey, parseModelCostRates, SUBSCRIPTION_API_COST_PROVIDERS, } from './cost.js';
3
3
  import { dateKeyFromTimestamp, scanSessionsByCreatedRange, updateSessionsInDayChunks, } from './storage.js';
4
4
  import { periodStart } from './period.js';
5
5
  import { debug, isRecord, mapConcurrent, swallow } from './helpers.js';
@@ -48,13 +48,10 @@ export function createUsageService(deps) {
48
48
  const map = all.reduce((acc, provider) => {
49
49
  if (!isRecord(provider))
50
50
  return acc;
51
- const providerID = typeof provider.id === 'string'
52
- ? canonicalApiCostProviderID(provider.id)
53
- : undefined;
54
- if (!providerID)
55
- return acc;
56
- if (!SUBSCRIPTION_API_COST_PROVIDERS.has(providerID))
51
+ const rawProviderID = typeof provider.id === 'string' ? provider.id : undefined;
52
+ if (!rawProviderID)
57
53
  return acc;
54
+ const canonicalProviderID = canonicalApiCostProviderID(rawProviderID);
58
55
  const models = provider.models;
59
56
  if (!isRecord(models))
60
57
  return acc;
@@ -65,9 +62,15 @@ export function createUsageService(deps) {
65
62
  if (!rates)
66
63
  continue;
67
64
  const modelID = typeof modelValue.id === 'string' ? modelValue.id : modelKey;
68
- acc[modelCostKey(providerID, modelID)] = rates;
65
+ acc[modelCostKey(rawProviderID, modelID)] = rates;
69
66
  if (modelKey !== modelID) {
70
- acc[modelCostKey(providerID, modelKey)] = rates;
67
+ acc[modelCostKey(rawProviderID, modelKey)] = rates;
68
+ }
69
+ if (canonicalProviderID !== rawProviderID) {
70
+ acc[modelCostKey(canonicalProviderID, modelID)] = rates;
71
+ if (modelKey !== modelID) {
72
+ acc[modelCostKey(canonicalProviderID, modelKey)] = rates;
73
+ }
71
74
  }
72
75
  }
73
76
  return acc;
@@ -78,7 +81,8 @@ export function createUsageService(deps) {
78
81
  const providerID = canonicalApiCostProviderID(message.providerID);
79
82
  if (!SUBSCRIPTION_API_COST_PROVIDERS.has(providerID))
80
83
  return 0;
81
- const rates = modelCostMap[modelCostKey(providerID, message.modelID)];
84
+ const rates = modelCostMap[modelCostKey(message.providerID, message.modelID)] ||
85
+ modelCostMap[modelCostKey(providerID, message.modelID)];
82
86
  if (!rates) {
83
87
  const key = modelCostKey(providerID, message.modelID);
84
88
  if (!missingApiCostRateKeys.has(key)) {
@@ -89,6 +93,30 @@ export function createUsageService(deps) {
89
93
  }
90
94
  return calcEquivalentApiCostForMessage(message, rates);
91
95
  };
96
+ const classifyCacheMode = (message, modelCostMap) => {
97
+ const canonicalProviderID = canonicalApiCostProviderID(message.providerID);
98
+ const baseRates = modelCostMap[modelCostKey(message.providerID, message.modelID)] ||
99
+ modelCostMap[modelCostKey(canonicalProviderID, message.modelID)];
100
+ const effectiveRates = baseRates &&
101
+ message.tokens.input + message.tokens.cache.read > 200_000 &&
102
+ baseRates.contextOver200k
103
+ ? baseRates.contextOver200k
104
+ : baseRates;
105
+ const fromRates = cacheCoverageModeFromRates(effectiveRates);
106
+ if (fromRates !== 'none')
107
+ return fromRates;
108
+ if (message.tokens.cache.write > 0)
109
+ return 'read-write';
110
+ if (message.tokens.cache.read <= 0)
111
+ return 'none';
112
+ if (canonicalProviderID === 'anthropic' ||
113
+ message.modelID.toLowerCase().includes('claude')) {
114
+ return 'read-write';
115
+ }
116
+ if (canonicalProviderID === 'openai')
117
+ return 'read-only';
118
+ return 'none';
119
+ };
92
120
  const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
93
121
  const decodeTokens = (value) => {
94
122
  if (!isRecord(value))
@@ -221,6 +249,7 @@ export function createUsageService(deps) {
221
249
  }
222
250
  const { usage, cursor } = summarizeMessagesIncremental(entries, sessionState?.usage, sessionState?.cursor, forceRescan, {
223
251
  calcApiCost: (message) => calcEquivalentApiCost(message, modelCostMap),
252
+ classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
224
253
  });
225
254
  usage.sessionCount = 1;
226
255
  // Update cursor in state
@@ -377,6 +406,7 @@ export function createUsageService(deps) {
377
406
  }
378
407
  const { usage: computed, cursor } = summarizeMessagesIncremental(entries, undefined, undefined, true, {
379
408
  calcApiCost: (message) => calcEquivalentApiCost(message, modelCostMap),
409
+ classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
380
410
  });
381
411
  return {
382
412
  sessionID: session.sessionID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "1.13.10",
3
+ "version": "2.0.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",