@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 +10 -8
- package/dist/cost.d.ts +2 -0
- package/dist/cost.js +9 -0
- package/dist/format.d.ts +1 -1
- package/dist/format.js +42 -0
- package/dist/storage_parse.js +33 -0
- package/dist/title.js +2 -0
- package/dist/types.d.ts +13 -0
- package/dist/usage.d.ts +9 -2
- package/dist/usage.js +110 -1
- package/dist/usage_service.js +40 -10
- package/package.json +1 -1
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
|
-
-
|
|
62
|
-
-
|
|
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
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({
|
package/dist/storage_parse.js
CHANGED
|
@@ -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
|
+
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 =
|
|
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,
|
package/dist/usage_service.js
CHANGED
|
@@ -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
|
|
52
|
-
|
|
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(
|
|
65
|
+
acc[modelCostKey(rawProviderID, modelID)] = rates;
|
|
69
66
|
if (modelKey !== modelID) {
|
|
70
|
-
acc[modelCostKey(
|
|
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,
|