@leo000001/opencode-quota-sidebar 1.13.8 → 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/dist/cost.d.ts CHANGED
@@ -1,6 +1,7 @@
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
- export declare function canonicalApiCostProviderID(providerID: string, npmPackage?: string): string;
4
+ export declare function canonicalApiCostProviderID(providerID: string): string;
4
5
  export type ModelCostRates = {
5
6
  input: number;
6
7
  output: number;
@@ -13,8 +14,8 @@ export type ModelCostRates = {
13
14
  cacheWrite: number;
14
15
  };
15
16
  };
16
- export declare function openAIServiceTierFromMessage(message: AssistantMessage): string | undefined;
17
17
  export declare function modelCostKey(providerID: string, modelID: string): string;
18
18
  export declare function parseModelCostRates(value: unknown): ModelCostRates | undefined;
19
19
  export declare function guessModelCostDivisor(rates: ModelCostRates): 1 | 1000000;
20
- export declare function calcEquivalentApiCostForMessage(message: AssistantMessage, rates: ModelCostRates, canonicalProviderID?: string, serviceTier?: string | undefined): number;
20
+ export declare function cacheCoverageModeFromRates(rates: ModelCostRates | undefined): CacheCoverageMode;
21
+ export declare function calcEquivalentApiCostForMessage(message: AssistantMessage, rates: ModelCostRates): number;
package/dist/cost.js CHANGED
@@ -5,20 +5,7 @@ function normalizeKnownProviderID(providerID) {
5
5
  return 'github-copilot';
6
6
  return providerID;
7
7
  }
8
- function canonicalProviderByNpmPackage(npmPackage) {
9
- const normalized = npmPackage.trim().toLowerCase();
10
- if (normalized === '@ai-sdk/openai')
11
- return 'openai';
12
- if (normalized === '@ai-sdk/anthropic')
13
- return 'anthropic';
14
- return undefined;
15
- }
16
- export function canonicalApiCostProviderID(providerID, npmPackage) {
17
- const byPackage = typeof npmPackage === 'string'
18
- ? canonicalProviderByNpmPackage(npmPackage)
19
- : undefined;
20
- if (byPackage)
21
- return byPackage;
8
+ export function canonicalApiCostProviderID(providerID) {
22
9
  const normalized = normalizeKnownProviderID(providerID);
23
10
  if (SUBSCRIPTION_API_COST_PROVIDERS.has(normalized))
24
11
  return normalized;
@@ -32,11 +19,6 @@ export function canonicalApiCostProviderID(providerID, npmPackage) {
32
19
  }
33
20
  return normalized;
34
21
  }
35
- export function openAIServiceTierFromMessage(message) {
36
- const info = message;
37
- return (info.providerMetadata?.openai?.serviceTier ??
38
- info.providerMetadata?.openai?.service_tier);
39
- }
40
22
  export function modelCostKey(providerID, modelID) {
41
23
  return `${providerID}:${modelID}`;
42
24
  }
@@ -96,20 +78,27 @@ export function guessModelCostDivisor(rates) {
96
78
  ? MODEL_COST_DIVISOR_PER_MILLION
97
79
  : MODEL_COST_DIVISOR_PER_TOKEN;
98
80
  }
99
- export function calcEquivalentApiCostForMessage(message, rates, canonicalProviderID = message.providerID, serviceTier = openAIServiceTierFromMessage(message)) {
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
+ }
90
+ export function calcEquivalentApiCostForMessage(message, rates) {
100
91
  const effectiveRates = message.tokens.input > 200_000 && rates.contextOver200k
101
92
  ? rates.contextOver200k
102
93
  : rates;
103
- const priorityMultiplier = canonicalProviderID === 'openai' && serviceTier === 'priority' ? 2 : 1;
104
94
  // For providers that expose reasoning tokens separately, they are still
105
95
  // billed as output/completion tokens (same unit price). Our UI also merges
106
96
  // reasoning into the single Output statistic, so API cost should match that.
107
97
  const billedOutput = message.tokens.output + message.tokens.reasoning;
108
- const rawCost = (message.tokens.input * effectiveRates.input +
98
+ const rawCost = message.tokens.input * effectiveRates.input +
109
99
  billedOutput * effectiveRates.output +
110
100
  message.tokens.cache.read * effectiveRates.cacheRead +
111
- message.tokens.cache.write * effectiveRates.cacheWrite) *
112
- priorityMultiplier;
101
+ message.tokens.cache.write * effectiveRates.cacheWrite;
113
102
  const divisor = guessModelCostDivisor(effectiveRates);
114
103
  const normalized = rawCost / divisor;
115
104
  return Number.isFinite(normalized) && normalized > 0 ? normalized : 0;
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, openAIServiceTierFromMessage, 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';
@@ -30,7 +30,7 @@ export function createUsageService(deps) {
30
30
  return cached;
31
31
  const providerClient = deps.client;
32
32
  if (!providerClient.provider?.list) {
33
- return modelCostCache.set({ rates: {}, providerAliases: {}, modelServiceTiers: {} }, 30_000);
33
+ return modelCostCache.set({}, 30_000);
34
34
  }
35
35
  const response = await providerClient.provider
36
36
  .list({
@@ -45,61 +45,13 @@ export function createUsageService(deps) {
45
45
  Array.isArray(response.data.all)
46
46
  ? response.data.all
47
47
  : [];
48
- const providerAliases = all.reduce((acc, provider) => {
48
+ const map = all.reduce((acc, provider) => {
49
49
  if (!isRecord(provider))
50
50
  return acc;
51
- if (typeof provider.id !== 'string')
52
- return acc;
53
- const canonical = canonicalApiCostProviderID(provider.id, typeof provider.npm === 'string' ? provider.npm : undefined);
54
- if (!SUBSCRIPTION_API_COST_PROVIDERS.has(canonical))
55
- return acc;
56
- acc[provider.id] = canonical;
57
- return acc;
58
- }, {});
59
- const modelServiceTiers = all.reduce((acc, provider) => {
60
- if (!isRecord(provider))
61
- return acc;
62
- const providerID = typeof provider.id === 'string'
63
- ? (providerAliases[provider.id] ??
64
- canonicalApiCostProviderID(provider.id, typeof provider.npm === 'string' ? provider.npm : undefined))
65
- : undefined;
66
- if (!providerID)
67
- return acc;
68
- if (!SUBSCRIPTION_API_COST_PROVIDERS.has(providerID))
69
- return acc;
70
- const models = provider.models;
71
- if (!isRecord(models))
72
- return acc;
73
- for (const [modelKey, modelValue] of Object.entries(models)) {
74
- if (!isRecord(modelValue))
75
- continue;
76
- const options = isRecord(modelValue.options)
77
- ? modelValue.options
78
- : undefined;
79
- const serviceTier = typeof options?.serviceTier === 'string'
80
- ? options.serviceTier
81
- : undefined;
82
- if (!serviceTier)
83
- continue;
84
- const modelID = typeof modelValue.id === 'string' ? modelValue.id : modelKey;
85
- acc[modelCostKey(providerID, modelID)] = serviceTier;
86
- if (modelKey !== modelID) {
87
- acc[modelCostKey(providerID, modelKey)] = serviceTier;
88
- }
89
- }
90
- return acc;
91
- }, {});
92
- const rates = all.reduce((acc, provider) => {
93
- if (!isRecord(provider))
94
- return acc;
95
- const providerID = typeof provider.id === 'string'
96
- ? (providerAliases[provider.id] ??
97
- canonicalApiCostProviderID(provider.id, typeof provider.npm === 'string' ? provider.npm : undefined))
98
- : undefined;
99
- if (!providerID)
100
- return acc;
101
- if (!SUBSCRIPTION_API_COST_PROVIDERS.has(providerID))
51
+ const rawProviderID = typeof provider.id === 'string' ? provider.id : undefined;
52
+ if (!rawProviderID)
102
53
  return acc;
54
+ const canonicalProviderID = canonicalApiCostProviderID(rawProviderID);
103
55
  const models = provider.models;
104
56
  if (!isRecord(models))
105
57
  return acc;
@@ -110,22 +62,27 @@ export function createUsageService(deps) {
110
62
  if (!rates)
111
63
  continue;
112
64
  const modelID = typeof modelValue.id === 'string' ? modelValue.id : modelKey;
113
- acc[modelCostKey(providerID, modelID)] = rates;
65
+ acc[modelCostKey(rawProviderID, modelID)] = rates;
114
66
  if (modelKey !== modelID) {
115
- 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
+ }
116
74
  }
117
75
  }
118
76
  return acc;
119
77
  }, {});
120
- return modelCostCache.set({ rates, providerAliases, modelServiceTiers }, Math.max(30_000, deps.config.quota.refreshMs));
78
+ return modelCostCache.set(map, Math.max(30_000, deps.config.quota.refreshMs));
121
79
  };
122
80
  const calcEquivalentApiCost = (message, modelCostMap) => {
123
- const providerID = typeof modelCostMap.providerAliases[message.providerID] === 'string'
124
- ? modelCostMap.providerAliases[message.providerID]
125
- : canonicalApiCostProviderID(message.providerID);
81
+ const providerID = canonicalApiCostProviderID(message.providerID);
126
82
  if (!SUBSCRIPTION_API_COST_PROVIDERS.has(providerID))
127
83
  return 0;
128
- const rates = modelCostMap.rates[modelCostKey(providerID, message.modelID)];
84
+ const rates = modelCostMap[modelCostKey(message.providerID, message.modelID)] ||
85
+ modelCostMap[modelCostKey(providerID, message.modelID)];
129
86
  if (!rates) {
130
87
  const key = modelCostKey(providerID, message.modelID);
131
88
  if (!missingApiCostRateKeys.has(key)) {
@@ -134,9 +91,31 @@ export function createUsageService(deps) {
134
91
  }
135
92
  return 0;
136
93
  }
137
- const serviceTier = openAIServiceTierFromMessage(message) ??
138
- modelCostMap.modelServiceTiers[modelCostKey(providerID, message.modelID)];
139
- return calcEquivalentApiCostForMessage(message, rates, providerID, serviceTier);
94
+ return calcEquivalentApiCostForMessage(message, rates);
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';
140
119
  };
141
120
  const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
142
121
  const decodeTokens = (value) => {
@@ -192,34 +171,12 @@ export function createUsageService(deps) {
192
171
  tokens,
193
172
  };
194
173
  };
195
- const extractProviderMetadata = (parts) => {
196
- if (!Array.isArray(parts))
197
- return undefined;
198
- for (const part of parts) {
199
- if (!isRecord(part))
200
- continue;
201
- const meta = part.metadata;
202
- if (isRecord(meta))
203
- return meta;
204
- const stateMeta = isRecord(part.state)
205
- ? part.state?.metadata
206
- : undefined;
207
- if (isRecord(stateMeta))
208
- return stateMeta;
209
- }
210
- return undefined;
211
- };
212
174
  const decodeMessageEntry = (value) => {
213
175
  if (!isRecord(value))
214
176
  return undefined;
215
177
  const decoded = decodeMessageInfo(value.info);
216
178
  if (!decoded)
217
179
  return undefined;
218
- const metadata = extractProviderMetadata(value.parts);
219
- if (metadata && decoded.role === 'assistant') {
220
- const msg = decoded;
221
- msg.providerMetadata = metadata;
222
- }
223
180
  return { info: decoded };
224
181
  };
225
182
  const decodeMessageEntries = (value) => {
@@ -292,6 +249,7 @@ export function createUsageService(deps) {
292
249
  }
293
250
  const { usage, cursor } = summarizeMessagesIncremental(entries, sessionState?.usage, sessionState?.cursor, forceRescan, {
294
251
  calcApiCost: (message) => calcEquivalentApiCost(message, modelCostMap),
252
+ classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
295
253
  });
296
254
  usage.sessionCount = 1;
297
255
  // Update cursor in state
@@ -448,6 +406,7 @@ export function createUsageService(deps) {
448
406
  }
449
407
  const { usage: computed, cursor } = summarizeMessagesIncremental(entries, undefined, undefined, true, {
450
408
  calcApiCost: (message) => calcEquivalentApiCost(message, modelCostMap),
409
+ classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
451
410
  });
452
411
  return {
453
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.8",
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",