@leo000001/opencode-quota-sidebar 2.0.7 → 2.0.9
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 +2 -1
- package/dist/cost.js +28 -2
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +1 -1
- package/dist/usage_service.js +122 -44
- package/package.json +1 -1
package/dist/cost.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AssistantMessage } from '@opencode-ai/sdk';
|
|
2
2
|
import type { CacheCoverageMode } from './types.js';
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const API_COST_ENABLED_PROVIDERS: Set<string>;
|
|
4
4
|
export declare function canonicalApiCostProviderID(providerID: string): string;
|
|
5
5
|
export type ModelCostRates = {
|
|
6
6
|
input: number;
|
|
@@ -15,6 +15,7 @@ export type ModelCostRates = {
|
|
|
15
15
|
};
|
|
16
16
|
};
|
|
17
17
|
export declare function modelCostKey(providerID: string, modelID: string): string;
|
|
18
|
+
export declare function modelCostLookupKeys(providerID: string, modelID: string): string[];
|
|
18
19
|
export declare function parseModelCostRates(value: unknown): ModelCostRates | undefined;
|
|
19
20
|
export declare function guessModelCostDivisor(rates: ModelCostRates): 1 | 1000000;
|
|
20
21
|
export declare function cacheCoverageModeFromRates(rates: ModelCostRates | undefined): CacheCoverageMode;
|
package/dist/cost.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { asNumber, isRecord } from './helpers.js';
|
|
2
|
-
export const
|
|
2
|
+
export const API_COST_ENABLED_PROVIDERS = new Set([
|
|
3
|
+
'openai',
|
|
4
|
+
'anthropic',
|
|
5
|
+
'kimi-for-coding',
|
|
6
|
+
]);
|
|
7
|
+
const MODEL_COST_RATE_ALIASES = {
|
|
8
|
+
'kimi-for-coding:k2p5': ['moonshotai-cn:kimi-k2.5'],
|
|
9
|
+
'kimi-for-coding:kimi-k2-thinking': ['moonshotai-cn:kimi-k2-thinking'],
|
|
10
|
+
};
|
|
3
11
|
function normalizeKnownProviderID(providerID) {
|
|
4
12
|
if (providerID.startsWith('github-copilot'))
|
|
5
13
|
return 'github-copilot';
|
|
@@ -7,7 +15,7 @@ function normalizeKnownProviderID(providerID) {
|
|
|
7
15
|
}
|
|
8
16
|
export function canonicalApiCostProviderID(providerID) {
|
|
9
17
|
const normalized = normalizeKnownProviderID(providerID);
|
|
10
|
-
if (
|
|
18
|
+
if (API_COST_ENABLED_PROVIDERS.has(normalized))
|
|
11
19
|
return normalized;
|
|
12
20
|
const lowered = providerID.toLowerCase();
|
|
13
21
|
if (lowered.includes('copilot'))
|
|
@@ -22,6 +30,24 @@ export function canonicalApiCostProviderID(providerID) {
|
|
|
22
30
|
export function modelCostKey(providerID, modelID) {
|
|
23
31
|
return `${providerID}:${modelID}`;
|
|
24
32
|
}
|
|
33
|
+
export function modelCostLookupKeys(providerID, modelID) {
|
|
34
|
+
const keys = [];
|
|
35
|
+
const canonicalProviderID = canonicalApiCostProviderID(providerID);
|
|
36
|
+
const push = (key) => {
|
|
37
|
+
if (!keys.includes(key))
|
|
38
|
+
keys.push(key);
|
|
39
|
+
};
|
|
40
|
+
push(modelCostKey(providerID, modelID));
|
|
41
|
+
if (canonicalProviderID !== providerID) {
|
|
42
|
+
push(modelCostKey(canonicalProviderID, modelID));
|
|
43
|
+
}
|
|
44
|
+
for (const key of [...keys]) {
|
|
45
|
+
for (const alias of MODEL_COST_RATE_ALIASES[key] || []) {
|
|
46
|
+
push(alias);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return keys;
|
|
50
|
+
}
|
|
25
51
|
export function parseModelCostRates(value) {
|
|
26
52
|
if (!isRecord(value))
|
|
27
53
|
return undefined;
|
package/dist/usage.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { CacheCoverageMetrics, CacheCoverageMode, CacheUsageBuckets, Cached
|
|
|
6
6
|
* fields). This is distinct from the plugin *state* version managed by the
|
|
7
7
|
* persistence layer; billing version only governs usage-cache staleness.
|
|
8
8
|
*/
|
|
9
|
-
export declare const USAGE_BILLING_CACHE_VERSION =
|
|
9
|
+
export declare const USAGE_BILLING_CACHE_VERSION = 5;
|
|
10
10
|
export type ProviderUsage = {
|
|
11
11
|
providerID: string;
|
|
12
12
|
input: number;
|
package/dist/usage.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* fields). This is distinct from the plugin *state* version managed by the
|
|
5
5
|
* persistence layer; billing version only governs usage-cache staleness.
|
|
6
6
|
*/
|
|
7
|
-
export const USAGE_BILLING_CACHE_VERSION =
|
|
7
|
+
export const USAGE_BILLING_CACHE_VERSION = 5;
|
|
8
8
|
function emptyCacheUsageBucket() {
|
|
9
9
|
return {
|
|
10
10
|
input: 0,
|
package/dist/usage_service.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { TtlValueCache } from './cache.js';
|
|
2
|
-
import { cacheCoverageModeFromRates, calcEquivalentApiCostForMessage, canonicalApiCostProviderID, modelCostKey, parseModelCostRates,
|
|
3
|
-
import { dateKeyFromTimestamp, scanAllSessions, updateSessionsInDayChunks, } from './storage.js';
|
|
2
|
+
import { API_COST_ENABLED_PROVIDERS, cacheCoverageModeFromRates, calcEquivalentApiCostForMessage, canonicalApiCostProviderID, modelCostLookupKeys, modelCostKey, parseModelCostRates, } from './cost.js';
|
|
3
|
+
import { deleteSessionFromDayChunk, dateKeyFromTimestamp, scanAllSessions, updateSessionsInDayChunks, } from './storage.js';
|
|
4
4
|
import { periodStart } from './period.js';
|
|
5
|
-
import { debug, isRecord, mapConcurrent, swallow } from './helpers.js';
|
|
5
|
+
import { debug, debugError, isRecord, mapConcurrent, swallow, } from './helpers.js';
|
|
6
6
|
import { emptyUsageSummary, fromCachedSessionUsage, mergeUsage, summarizeMessagesInCompletedRange, summarizeMessagesIncremental, toCachedSessionUsage, USAGE_BILLING_CACHE_VERSION, } from './usage.js';
|
|
7
7
|
const READ_ONLY_CACHE_PROVIDERS = new Set([
|
|
8
8
|
'openai',
|
|
@@ -87,10 +87,11 @@ export function createUsageService(deps) {
|
|
|
87
87
|
};
|
|
88
88
|
const calcEquivalentApiCost = (message, modelCostMap) => {
|
|
89
89
|
const providerID = canonicalApiCostProviderID(message.providerID);
|
|
90
|
-
if (!
|
|
90
|
+
if (!API_COST_ENABLED_PROVIDERS.has(providerID))
|
|
91
91
|
return 0;
|
|
92
|
-
const rates =
|
|
93
|
-
modelCostMap[
|
|
92
|
+
const rates = modelCostLookupKeys(message.providerID, message.modelID)
|
|
93
|
+
.map((key) => modelCostMap[key])
|
|
94
|
+
.find(Boolean);
|
|
94
95
|
if (!rates) {
|
|
95
96
|
const key = modelCostKey(providerID, message.modelID);
|
|
96
97
|
if (!missingApiCostRateKeys.has(key)) {
|
|
@@ -103,8 +104,9 @@ export function createUsageService(deps) {
|
|
|
103
104
|
};
|
|
104
105
|
const classifyCacheMode = (message, modelCostMap) => {
|
|
105
106
|
const canonicalProviderID = canonicalApiCostProviderID(message.providerID);
|
|
106
|
-
const baseRates =
|
|
107
|
-
modelCostMap[
|
|
107
|
+
const baseRates = modelCostLookupKeys(message.providerID, message.modelID)
|
|
108
|
+
.map((key) => modelCostMap[key])
|
|
109
|
+
.find(Boolean);
|
|
108
110
|
const effectiveRates = baseRates &&
|
|
109
111
|
message.tokens.input + message.tokens.cache.read > 200_000 &&
|
|
110
112
|
baseRates.contextOver200k
|
|
@@ -218,18 +220,81 @@ export function createUsageService(deps) {
|
|
|
218
220
|
return undefined;
|
|
219
221
|
return decoded;
|
|
220
222
|
};
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
.messages({
|
|
224
|
-
path: { id: sessionID },
|
|
225
|
-
query: { directory: deps.directory },
|
|
226
|
-
throwOnError: true,
|
|
227
|
-
})
|
|
228
|
-
.catch(swallow('loadSessionEntries'));
|
|
229
|
-
if (!response)
|
|
223
|
+
const errorStatusCode = (value, seen = new Set()) => {
|
|
224
|
+
if (!isRecord(value) || seen.has(value))
|
|
230
225
|
return undefined;
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
seen.add(value);
|
|
227
|
+
const status = value.status;
|
|
228
|
+
if (typeof status === 'number' && Number.isFinite(status))
|
|
229
|
+
return status;
|
|
230
|
+
const statusCode = value.statusCode;
|
|
231
|
+
if (typeof statusCode === 'number' && Number.isFinite(statusCode)) {
|
|
232
|
+
return statusCode;
|
|
233
|
+
}
|
|
234
|
+
return (errorStatusCode(value.response, seen) ||
|
|
235
|
+
errorStatusCode(value.cause, seen) ||
|
|
236
|
+
errorStatusCode(value.error, seen));
|
|
237
|
+
};
|
|
238
|
+
const errorText = (value, seen = new Set()) => {
|
|
239
|
+
if (!value || seen.has(value))
|
|
240
|
+
return '';
|
|
241
|
+
if (typeof value === 'string')
|
|
242
|
+
return value;
|
|
243
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
244
|
+
return `${value}`;
|
|
245
|
+
if (value instanceof Error) {
|
|
246
|
+
seen.add(value);
|
|
247
|
+
return [
|
|
248
|
+
value.message,
|
|
249
|
+
errorText(value.cause, seen),
|
|
250
|
+
]
|
|
251
|
+
.filter(Boolean)
|
|
252
|
+
.join('\n');
|
|
253
|
+
}
|
|
254
|
+
if (!isRecord(value))
|
|
255
|
+
return '';
|
|
256
|
+
seen.add(value);
|
|
257
|
+
return [
|
|
258
|
+
typeof value.message === 'string' ? value.message : '',
|
|
259
|
+
typeof value.error === 'string' ? value.error : '',
|
|
260
|
+
typeof value.detail === 'string' ? value.detail : '',
|
|
261
|
+
typeof value.title === 'string' ? value.title : '',
|
|
262
|
+
errorText(value.response, seen),
|
|
263
|
+
errorText(value.data, seen),
|
|
264
|
+
errorText(value.cause, seen),
|
|
265
|
+
]
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.join('\n');
|
|
268
|
+
};
|
|
269
|
+
const isMissingSessionError = (error) => {
|
|
270
|
+
const status = errorStatusCode(error);
|
|
271
|
+
if (status === 404 || status === 410)
|
|
272
|
+
return true;
|
|
273
|
+
const text = errorText(error).toLowerCase();
|
|
274
|
+
if (!text)
|
|
275
|
+
return false;
|
|
276
|
+
return (/\b(session|conversation)\b.*\b(not found|missing|deleted|does not exist)\b/.test(text) ||
|
|
277
|
+
/\b(not found|missing|deleted|does not exist)\b.*\b(session|conversation)\b/.test(text));
|
|
278
|
+
};
|
|
279
|
+
const loadSessionEntries = async (sessionID) => {
|
|
280
|
+
try {
|
|
281
|
+
const response = await deps.client.session.messages({
|
|
282
|
+
path: { id: sessionID },
|
|
283
|
+
query: { directory: deps.directory },
|
|
284
|
+
throwOnError: true,
|
|
285
|
+
});
|
|
286
|
+
const data = response.data;
|
|
287
|
+
const entries = decodeMessageEntries(data);
|
|
288
|
+
if (!entries)
|
|
289
|
+
return { status: 'error' };
|
|
290
|
+
return { status: 'ok', entries };
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
debugError(`loadSessionEntries ${sessionID}`, error);
|
|
294
|
+
return {
|
|
295
|
+
status: isMissingSessionError(error) ? 'missing' : 'error',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
233
298
|
};
|
|
234
299
|
const persistSessionUsage = (sessionID, usage) => {
|
|
235
300
|
const sessionState = deps.state.sessions[sessionID];
|
|
@@ -246,23 +311,9 @@ export function createUsageService(deps) {
|
|
|
246
311
|
return false;
|
|
247
312
|
return cached.billingVersion === USAGE_BILLING_CACHE_VERSION;
|
|
248
313
|
};
|
|
249
|
-
const hasStaleZeroApiCost = (cached, modelCostMap) => {
|
|
250
|
-
if (!cached)
|
|
251
|
-
return false;
|
|
252
|
-
if (cached.apiCost > 0)
|
|
253
|
-
return false;
|
|
254
|
-
return Object.entries(cached.providers).some(([providerID, provider]) => {
|
|
255
|
-
if (provider.apiCost > 0)
|
|
256
|
-
return false;
|
|
257
|
-
const canonicalProviderID = canonicalApiCostProviderID(providerID);
|
|
258
|
-
if (!SUBSCRIPTION_API_COST_PROVIDERS.has(canonicalProviderID))
|
|
259
|
-
return false;
|
|
260
|
-
return Object.keys(modelCostMap).some((key) => key.startsWith(`${providerID}:`) ||
|
|
261
|
-
key.startsWith(`${canonicalProviderID}:`));
|
|
262
|
-
});
|
|
263
|
-
};
|
|
264
314
|
const summarizeSessionUsage = async (sessionID, generationAtStart, options) => {
|
|
265
|
-
const
|
|
315
|
+
const load = await loadSessionEntries(sessionID);
|
|
316
|
+
const entries = load.status === 'ok' ? load.entries : undefined;
|
|
266
317
|
const sessionState = deps.state.sessions[sessionID];
|
|
267
318
|
// If we can't load messages (transient API failure), fall back to cached
|
|
268
319
|
// usage if available and avoid mutating cursor/dirty state.
|
|
@@ -281,9 +332,7 @@ export function createUsageService(deps) {
|
|
|
281
332
|
return { usage: empty, persist: false };
|
|
282
333
|
}
|
|
283
334
|
const modelCostMap = await getModelCostMap();
|
|
284
|
-
const staleBillingCache = Boolean(sessionState?.usage) &&
|
|
285
|
-
(!isUsageBillingCurrent(sessionState?.usage) ||
|
|
286
|
-
hasStaleZeroApiCost(sessionState?.usage, modelCostMap));
|
|
335
|
+
const staleBillingCache = Boolean(sessionState?.usage) && !isUsageBillingCurrent(sessionState?.usage);
|
|
287
336
|
const forceRescan = forceRescanSessions.has(sessionID) || staleBillingCache;
|
|
288
337
|
if (forceRescan)
|
|
289
338
|
forceRescanSessions.delete(sessionID);
|
|
@@ -406,7 +455,7 @@ export function createUsageService(deps) {
|
|
|
406
455
|
return true;
|
|
407
456
|
return providerIDs.some((providerID) => {
|
|
408
457
|
const canonical = canonicalApiCostProviderID(providerID);
|
|
409
|
-
return
|
|
458
|
+
return API_COST_ENABLED_PROVIDERS.has(canonical);
|
|
410
459
|
});
|
|
411
460
|
};
|
|
412
461
|
const shouldRecomputeUsageCache = (cached) => {
|
|
@@ -426,8 +475,8 @@ export function createUsageService(deps) {
|
|
|
426
475
|
};
|
|
427
476
|
if (sessions.length > 0) {
|
|
428
477
|
const fetched = await mapConcurrent(sessions, RANGE_USAGE_CONCURRENCY, async (session) => {
|
|
429
|
-
const
|
|
430
|
-
if (
|
|
478
|
+
const load = await loadSessionEntries(session.sessionID);
|
|
479
|
+
if (load.status !== 'ok') {
|
|
431
480
|
return {
|
|
432
481
|
sessionID: session.sessionID,
|
|
433
482
|
dateKey: session.dateKey,
|
|
@@ -436,16 +485,19 @@ export function createUsageService(deps) {
|
|
|
436
485
|
dirty: session.state.dirty === true,
|
|
437
486
|
computed: emptyUsageSummary(),
|
|
438
487
|
fullUsage: undefined,
|
|
439
|
-
loadFailed:
|
|
488
|
+
loadFailed: load.status === 'error',
|
|
489
|
+
missing: load.status === 'missing',
|
|
440
490
|
persist: false,
|
|
441
491
|
cursor: undefined,
|
|
442
492
|
};
|
|
443
493
|
}
|
|
494
|
+
const entries = load.entries;
|
|
444
495
|
const computed = summarizeMessagesInCompletedRange(entries, startAt, endAt, 0, {
|
|
445
496
|
calcApiCost: (message) => calcEquivalentApiCost(message, modelCostMap),
|
|
446
497
|
classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
|
|
447
498
|
});
|
|
448
|
-
const shouldPersistFullUsage = !session.state.usage ||
|
|
499
|
+
const shouldPersistFullUsage = !session.state.usage ||
|
|
500
|
+
shouldRecomputeUsageCache(session.state.usage);
|
|
449
501
|
if (!shouldPersistFullUsage) {
|
|
450
502
|
return {
|
|
451
503
|
sessionID: session.sessionID,
|
|
@@ -456,6 +508,7 @@ export function createUsageService(deps) {
|
|
|
456
508
|
computed,
|
|
457
509
|
fullUsage: undefined,
|
|
458
510
|
loadFailed: false,
|
|
511
|
+
missing: false,
|
|
459
512
|
persist: false,
|
|
460
513
|
cursor: session.state.cursor,
|
|
461
514
|
};
|
|
@@ -473,10 +526,35 @@ export function createUsageService(deps) {
|
|
|
473
526
|
computed,
|
|
474
527
|
fullUsage,
|
|
475
528
|
loadFailed: false,
|
|
529
|
+
missing: false,
|
|
476
530
|
persist: true,
|
|
477
531
|
cursor,
|
|
478
532
|
};
|
|
479
533
|
});
|
|
534
|
+
const missingSessions = fetched.filter((item) => item.missing);
|
|
535
|
+
if (missingSessions.length > 0) {
|
|
536
|
+
let stateChanged = false;
|
|
537
|
+
for (const missing of missingSessions) {
|
|
538
|
+
deps.state.deletedSessionDateMap[missing.sessionID] = missing.dateKey;
|
|
539
|
+
delete deps.state.sessions[missing.sessionID];
|
|
540
|
+
delete deps.state.sessionDateMap[missing.sessionID];
|
|
541
|
+
deps.persistence.markDirty(missing.dateKey);
|
|
542
|
+
forgetSession(missing.sessionID);
|
|
543
|
+
stateChanged = true;
|
|
544
|
+
}
|
|
545
|
+
await Promise.all(missingSessions.map(async (missing) => {
|
|
546
|
+
const deletedFromChunk = await deleteSessionFromDayChunk(deps.statePath, missing.sessionID, missing.dateKey).catch((error) => {
|
|
547
|
+
swallow('deleteSessionFromDayChunk')(error);
|
|
548
|
+
return false;
|
|
549
|
+
});
|
|
550
|
+
if (!deletedFromChunk)
|
|
551
|
+
return;
|
|
552
|
+
delete deps.state.deletedSessionDateMap[missing.sessionID];
|
|
553
|
+
stateChanged = true;
|
|
554
|
+
}));
|
|
555
|
+
if (stateChanged)
|
|
556
|
+
deps.persistence.scheduleSave();
|
|
557
|
+
}
|
|
480
558
|
const failedLoads = fetched.filter((item) => {
|
|
481
559
|
if (!item.loadFailed)
|
|
482
560
|
return false;
|
|
@@ -493,7 +571,7 @@ export function createUsageService(deps) {
|
|
|
493
571
|
}
|
|
494
572
|
let dirty = false;
|
|
495
573
|
const diskOnlyUpdates = [];
|
|
496
|
-
for (const { sessionID, dateKey, computed, fullUsage, persist, cursor } of fetched) {
|
|
574
|
+
for (const { sessionID, dateKey, computed, fullUsage, persist, cursor, } of fetched) {
|
|
497
575
|
if (computed.assistantMessages > 0) {
|
|
498
576
|
computed.sessionCount = 1;
|
|
499
577
|
mergeUsage(usage, computed);
|