@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/README.md +475 -477
- package/dist/cost.d.ts +4 -3
- package/dist/cost.js +13 -24
- 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 +45 -86
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
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,
|
|
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({
|
|
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
|
|
48
|
+
const map = all.reduce((acc, provider) => {
|
|
49
49
|
if (!isRecord(provider))
|
|
50
50
|
return acc;
|
|
51
|
-
|
|
52
|
-
|
|
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(
|
|
65
|
+
acc[modelCostKey(rawProviderID, modelID)] = rates;
|
|
114
66
|
if (modelKey !== modelID) {
|
|
115
|
-
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
|
+
}
|
|
116
74
|
}
|
|
117
75
|
}
|
|
118
76
|
return acc;
|
|
119
77
|
}, {});
|
|
120
|
-
return modelCostCache.set(
|
|
78
|
+
return modelCostCache.set(map, Math.max(30_000, deps.config.quota.refreshMs));
|
|
121
79
|
};
|
|
122
80
|
const calcEquivalentApiCost = (message, modelCostMap) => {
|
|
123
|
-
const providerID =
|
|
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
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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,
|