@leo000001/opencode-quota-sidebar 2.0.0 → 2.0.1
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 +14 -12
- package/dist/cost.js +2 -1
- package/dist/format.js +37 -22
- package/dist/index.js +24 -2
- package/dist/quota.js +3 -5
- package/dist/quota_service.js +65 -18
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +74 -9
- package/dist/storage_chunks.js +20 -9
- package/dist/storage_parse.js +3 -1
- package/dist/title.js +37 -15
- package/dist/title_apply.d.ts +2 -0
- package/dist/title_apply.js +43 -13
- package/dist/title_refresh.d.ts +2 -0
- package/dist/title_refresh.js +12 -1
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +8 -4
- package/dist/types.d.ts +25 -1
- package/dist/usage.d.ts +12 -6
- package/dist/usage.js +63 -13
- package/dist/usage_service.js +121 -48
- package/package.json +1 -1
package/dist/usage_service.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { TtlValueCache } from './cache.js';
|
|
2
2
|
import { cacheCoverageModeFromRates, calcEquivalentApiCostForMessage, canonicalApiCostProviderID, modelCostKey, parseModelCostRates, SUBSCRIPTION_API_COST_PROVIDERS, } from './cost.js';
|
|
3
|
-
import { dateKeyFromTimestamp,
|
|
3
|
+
import { dateKeyFromTimestamp, scanAllSessions, updateSessionsInDayChunks, } from './storage.js';
|
|
4
4
|
import { periodStart } from './period.js';
|
|
5
5
|
import { debug, isRecord, mapConcurrent, swallow } from './helpers.js';
|
|
6
|
-
import { emptyUsageSummary, fromCachedSessionUsage, mergeUsage, summarizeMessagesIncremental, toCachedSessionUsage, USAGE_BILLING_CACHE_VERSION, } from './usage.js';
|
|
6
|
+
import { emptyUsageSummary, fromCachedSessionUsage, mergeUsage, summarizeMessagesInCompletedRange, summarizeMessagesIncremental, toCachedSessionUsage, USAGE_BILLING_CACHE_VERSION, } from './usage.js';
|
|
7
|
+
const READ_ONLY_CACHE_PROVIDERS = new Set([
|
|
8
|
+
'openai',
|
|
9
|
+
'github-copilot',
|
|
10
|
+
'venice',
|
|
11
|
+
'openrouter',
|
|
12
|
+
]);
|
|
7
13
|
export function createUsageService(deps) {
|
|
8
14
|
const forceRescanSessions = new Set();
|
|
9
15
|
const dirtyGeneration = new Map();
|
|
@@ -12,6 +18,8 @@ export function createUsageService(deps) {
|
|
|
12
18
|
dirtyGeneration.set(sessionID, (dirtyGeneration.get(sessionID) || 0) + 1);
|
|
13
19
|
};
|
|
14
20
|
const isDirty = (sessionID) => {
|
|
21
|
+
if (deps.state.sessions[sessionID]?.dirty)
|
|
22
|
+
return true;
|
|
15
23
|
return ((dirtyGeneration.get(sessionID) || 0) !==
|
|
16
24
|
(cleanGeneration.get(sessionID) || 0));
|
|
17
25
|
};
|
|
@@ -109,13 +117,19 @@ export function createUsageService(deps) {
|
|
|
109
117
|
return 'read-write';
|
|
110
118
|
if (message.tokens.cache.read <= 0)
|
|
111
119
|
return 'none';
|
|
120
|
+
const rawProviderID = message.providerID.toLowerCase();
|
|
121
|
+
if (READ_ONLY_CACHE_PROVIDERS.has(canonicalProviderID) ||
|
|
122
|
+
READ_ONLY_CACHE_PROVIDERS.has(rawProviderID)) {
|
|
123
|
+
return 'read-only';
|
|
124
|
+
}
|
|
125
|
+
// Heuristic fallback: classify by provider identity when pricing is missing.
|
|
112
126
|
if (canonicalProviderID === 'anthropic' ||
|
|
113
127
|
message.modelID.toLowerCase().includes('claude')) {
|
|
114
128
|
return 'read-write';
|
|
115
129
|
}
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
return '
|
|
130
|
+
// Last resort: if the message has cache.read tokens from an unknown provider,
|
|
131
|
+
// treat it as read-only (the safer default — avoids inflating Cache Coverage).
|
|
132
|
+
return 'read-only';
|
|
119
133
|
};
|
|
120
134
|
const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
121
135
|
const decodeTokens = (value) => {
|
|
@@ -145,10 +159,6 @@ export function createUsageService(deps) {
|
|
|
145
159
|
return undefined;
|
|
146
160
|
if (typeof value.role !== 'string')
|
|
147
161
|
return undefined;
|
|
148
|
-
if (typeof value.providerID !== 'string')
|
|
149
|
-
return undefined;
|
|
150
|
-
if (typeof value.modelID !== 'string')
|
|
151
|
-
return undefined;
|
|
152
162
|
if (!isRecord(value.time))
|
|
153
163
|
return undefined;
|
|
154
164
|
if (!isFiniteNumber(value.time.created))
|
|
@@ -157,6 +167,19 @@ export function createUsageService(deps) {
|
|
|
157
167
|
!isFiniteNumber(value.time.completed)) {
|
|
158
168
|
return undefined;
|
|
159
169
|
}
|
|
170
|
+
if (value.role !== 'assistant') {
|
|
171
|
+
return {
|
|
172
|
+
...value,
|
|
173
|
+
time: {
|
|
174
|
+
created: value.time.created,
|
|
175
|
+
completed: value.time.completed,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (typeof value.providerID !== 'string')
|
|
180
|
+
return undefined;
|
|
181
|
+
if (typeof value.modelID !== 'string')
|
|
182
|
+
return undefined;
|
|
160
183
|
const tokens = decodeTokens(value.tokens);
|
|
161
184
|
if (!tokens)
|
|
162
185
|
return undefined;
|
|
@@ -222,6 +245,21 @@ export function createUsageService(deps) {
|
|
|
222
245
|
return false;
|
|
223
246
|
return cached.billingVersion === USAGE_BILLING_CACHE_VERSION;
|
|
224
247
|
};
|
|
248
|
+
const hasStaleZeroApiCost = (cached, modelCostMap) => {
|
|
249
|
+
if (!cached)
|
|
250
|
+
return false;
|
|
251
|
+
if (cached.apiCost > 0)
|
|
252
|
+
return false;
|
|
253
|
+
return Object.entries(cached.providers).some(([providerID, provider]) => {
|
|
254
|
+
if (provider.apiCost > 0)
|
|
255
|
+
return false;
|
|
256
|
+
const canonicalProviderID = canonicalApiCostProviderID(providerID);
|
|
257
|
+
if (!SUBSCRIPTION_API_COST_PROVIDERS.has(canonicalProviderID))
|
|
258
|
+
return false;
|
|
259
|
+
return Object.keys(modelCostMap).some((key) => key.startsWith(`${providerID}:`) ||
|
|
260
|
+
key.startsWith(`${canonicalProviderID}:`));
|
|
261
|
+
});
|
|
262
|
+
};
|
|
225
263
|
const summarizeSessionUsage = async (sessionID, generationAtStart) => {
|
|
226
264
|
const entries = await loadSessionEntries(sessionID);
|
|
227
265
|
const sessionState = deps.state.sessions[sessionID];
|
|
@@ -240,7 +278,8 @@ export function createUsageService(deps) {
|
|
|
240
278
|
}
|
|
241
279
|
const modelCostMap = await getModelCostMap();
|
|
242
280
|
const staleBillingCache = Boolean(sessionState?.usage) &&
|
|
243
|
-
!isUsageBillingCurrent(sessionState?.usage)
|
|
281
|
+
(!isUsageBillingCurrent(sessionState?.usage) ||
|
|
282
|
+
hasStaleZeroApiCost(sessionState?.usage, modelCostMap));
|
|
244
283
|
const forceRescan = forceRescanSessions.has(sessionID) || staleBillingCache;
|
|
245
284
|
if (forceRescan)
|
|
246
285
|
forceRescanSessions.delete(sessionID);
|
|
@@ -255,6 +294,7 @@ export function createUsageService(deps) {
|
|
|
255
294
|
// Update cursor in state
|
|
256
295
|
if (sessionState) {
|
|
257
296
|
sessionState.cursor = cursor;
|
|
297
|
+
sessionState.dirty = false;
|
|
258
298
|
}
|
|
259
299
|
if ((dirtyGeneration.get(sessionID) || 0) === generationAtStart) {
|
|
260
300
|
cleanGeneration.set(sessionID, generationAtStart);
|
|
@@ -290,11 +330,16 @@ export function createUsageService(deps) {
|
|
|
290
330
|
const summarizeSessionUsageForDisplay = async (sessionID, includeChildren) => {
|
|
291
331
|
const root = await summarizeSessionUsageLocked(sessionID);
|
|
292
332
|
const usage = root.usage;
|
|
333
|
+
let dirty = false;
|
|
293
334
|
if (root.persist) {
|
|
294
335
|
persistSessionUsage(sessionID, toCachedSessionUsage(usage));
|
|
336
|
+
dirty = true;
|
|
295
337
|
}
|
|
296
|
-
if (!includeChildren)
|
|
338
|
+
if (!includeChildren) {
|
|
339
|
+
if (dirty)
|
|
340
|
+
deps.persistence.scheduleSave();
|
|
297
341
|
return usage;
|
|
342
|
+
}
|
|
298
343
|
const descendantIDs = await deps.descendantsResolver.listDescendantSessionIDs(sessionID, {
|
|
299
344
|
maxDepth: deps.config.sidebar.childrenMaxDepth,
|
|
300
345
|
maxSessions: deps.config.sidebar.childrenMaxSessions,
|
|
@@ -323,6 +368,7 @@ export function createUsageService(deps) {
|
|
|
323
368
|
const child = await summarizeSessionUsageLocked(childID);
|
|
324
369
|
if (child.persist) {
|
|
325
370
|
persistSessionUsage(childID, toCachedSessionUsage(child.usage));
|
|
371
|
+
dirty = true;
|
|
326
372
|
}
|
|
327
373
|
return child.usage;
|
|
328
374
|
});
|
|
@@ -330,15 +376,17 @@ export function createUsageService(deps) {
|
|
|
330
376
|
mergeUsage(merged, childUsage, { includeCost: false });
|
|
331
377
|
}
|
|
332
378
|
}
|
|
379
|
+
if (dirty)
|
|
380
|
+
deps.persistence.scheduleSave();
|
|
333
381
|
return merged;
|
|
334
382
|
};
|
|
335
383
|
const RANGE_USAGE_CONCURRENCY = 5;
|
|
336
384
|
const summarizeRangeUsage = async (period) => {
|
|
337
385
|
const startAt = periodStart(period);
|
|
386
|
+
const endAt = Date.now();
|
|
338
387
|
await deps.persistence.flushSave();
|
|
339
|
-
const sessions = await
|
|
388
|
+
const sessions = await scanAllSessions(deps.statePath, deps.state);
|
|
340
389
|
const usage = emptyUsageSummary();
|
|
341
|
-
usage.sessionCount = sessions.length;
|
|
342
390
|
const modelCostMap = await getModelCostMap();
|
|
343
391
|
const hasPricing = Object.keys(modelCostMap).length > 0;
|
|
344
392
|
const hasAnySubscriptionProvider = (cached) => {
|
|
@@ -367,62 +415,78 @@ export function createUsageService(deps) {
|
|
|
367
415
|
return false;
|
|
368
416
|
return true;
|
|
369
417
|
};
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (session.state.usage) {
|
|
373
|
-
if (shouldRecomputeUsageCache(session.state.usage)) {
|
|
374
|
-
needsFetch.push(session);
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
mergeUsage(usage, fromCachedSessionUsage(session.state.usage, 0));
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
needsFetch.push(session);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (needsFetch.length > 0) {
|
|
385
|
-
const fetched = await mapConcurrent(needsFetch, RANGE_USAGE_CONCURRENCY, async (session) => {
|
|
418
|
+
if (sessions.length > 0) {
|
|
419
|
+
const fetched = await mapConcurrent(sessions, RANGE_USAGE_CONCURRENCY, async (session) => {
|
|
386
420
|
const entries = await loadSessionEntries(session.sessionID);
|
|
387
421
|
if (!entries) {
|
|
388
|
-
if (session.state.usage) {
|
|
389
|
-
return {
|
|
390
|
-
sessionID: session.sessionID,
|
|
391
|
-
dateKey: session.dateKey,
|
|
392
|
-
computed: fromCachedSessionUsage(session.state.usage, 1),
|
|
393
|
-
persist: false,
|
|
394
|
-
cursor: session.state.cursor,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
const empty = emptyUsageSummary();
|
|
398
|
-
empty.sessionCount = 1;
|
|
399
422
|
return {
|
|
400
423
|
sessionID: session.sessionID,
|
|
401
424
|
dateKey: session.dateKey,
|
|
402
|
-
|
|
425
|
+
createdAt: session.state.createdAt,
|
|
426
|
+
lastMessageTime: session.state.cursor?.lastMessageTime,
|
|
427
|
+
computed: emptyUsageSummary(),
|
|
428
|
+
fullUsage: undefined,
|
|
429
|
+
loadFailed: true,
|
|
403
430
|
persist: false,
|
|
404
431
|
cursor: undefined,
|
|
405
432
|
};
|
|
406
433
|
}
|
|
407
|
-
const
|
|
434
|
+
const computed = summarizeMessagesInCompletedRange(entries, startAt, endAt, 0, {
|
|
435
|
+
calcApiCost: (message) => calcEquivalentApiCost(message, modelCostMap),
|
|
436
|
+
classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
|
|
437
|
+
});
|
|
438
|
+
const shouldPersistFullUsage = !session.state.usage || shouldRecomputeUsageCache(session.state.usage);
|
|
439
|
+
if (!shouldPersistFullUsage) {
|
|
440
|
+
return {
|
|
441
|
+
sessionID: session.sessionID,
|
|
442
|
+
dateKey: session.dateKey,
|
|
443
|
+
createdAt: session.state.createdAt,
|
|
444
|
+
lastMessageTime: session.state.cursor?.lastMessageTime,
|
|
445
|
+
computed,
|
|
446
|
+
fullUsage: undefined,
|
|
447
|
+
loadFailed: false,
|
|
448
|
+
persist: false,
|
|
449
|
+
cursor: session.state.cursor,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const { usage: fullUsage, cursor } = summarizeMessagesIncremental(entries, undefined, undefined, true, {
|
|
408
453
|
calcApiCost: (message) => calcEquivalentApiCost(message, modelCostMap),
|
|
409
454
|
classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
|
|
410
455
|
});
|
|
411
456
|
return {
|
|
412
457
|
sessionID: session.sessionID,
|
|
413
458
|
dateKey: session.dateKey,
|
|
459
|
+
createdAt: session.state.createdAt,
|
|
460
|
+
lastMessageTime: cursor.lastMessageTime,
|
|
414
461
|
computed,
|
|
462
|
+
fullUsage,
|
|
463
|
+
loadFailed: false,
|
|
415
464
|
persist: true,
|
|
416
465
|
cursor,
|
|
417
466
|
};
|
|
418
467
|
});
|
|
468
|
+
const failedLoads = fetched.filter((item) => {
|
|
469
|
+
if (!item.loadFailed)
|
|
470
|
+
return false;
|
|
471
|
+
const lastMessageTime = item.lastMessageTime;
|
|
472
|
+
if (typeof lastMessageTime === 'number' && lastMessageTime < startAt) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
return true;
|
|
476
|
+
});
|
|
477
|
+
if (failedLoads.length > 0) {
|
|
478
|
+
throw new Error(`range usage unavailable: failed to load ${failedLoads.length} session(s)`);
|
|
479
|
+
}
|
|
419
480
|
let dirty = false;
|
|
420
481
|
const diskOnlyUpdates = [];
|
|
421
|
-
for (const { sessionID, dateKey, computed, persist, cursor } of fetched) {
|
|
422
|
-
|
|
482
|
+
for (const { sessionID, dateKey, computed, fullUsage, persist, cursor } of fetched) {
|
|
483
|
+
if (computed.assistantMessages > 0) {
|
|
484
|
+
computed.sessionCount = 1;
|
|
485
|
+
mergeUsage(usage, computed);
|
|
486
|
+
}
|
|
423
487
|
const memoryState = deps.state.sessions[sessionID];
|
|
424
|
-
if (persist && memoryState) {
|
|
425
|
-
memoryState.usage = toCachedSessionUsage(
|
|
488
|
+
if (persist && fullUsage && memoryState) {
|
|
489
|
+
memoryState.usage = toCachedSessionUsage(fullUsage);
|
|
426
490
|
memoryState.cursor = cursor;
|
|
427
491
|
const resolvedDateKey = deps.state.sessionDateMap[sessionID] ||
|
|
428
492
|
dateKeyFromTimestamp(memoryState.createdAt);
|
|
@@ -430,11 +494,11 @@ export function createUsageService(deps) {
|
|
|
430
494
|
deps.persistence.markDirty(resolvedDateKey);
|
|
431
495
|
dirty = true;
|
|
432
496
|
}
|
|
433
|
-
else if (persist) {
|
|
497
|
+
else if (persist && fullUsage) {
|
|
434
498
|
diskOnlyUpdates.push({
|
|
435
499
|
sessionID,
|
|
436
500
|
dateKey,
|
|
437
|
-
usage: toCachedSessionUsage(
|
|
501
|
+
usage: toCachedSessionUsage(fullUsage),
|
|
438
502
|
cursor,
|
|
439
503
|
});
|
|
440
504
|
}
|
|
@@ -455,6 +519,15 @@ export function createUsageService(deps) {
|
|
|
455
519
|
};
|
|
456
520
|
const markSessionDirty = (sessionID) => {
|
|
457
521
|
bumpDirty(sessionID);
|
|
522
|
+
const sessionState = deps.state.sessions[sessionID];
|
|
523
|
+
if (sessionState && !sessionState.dirty) {
|
|
524
|
+
sessionState.dirty = true;
|
|
525
|
+
const dateKey = deps.state.sessionDateMap[sessionID] ||
|
|
526
|
+
dateKeyFromTimestamp(sessionState.createdAt);
|
|
527
|
+
deps.state.sessionDateMap[sessionID] = dateKey;
|
|
528
|
+
deps.persistence.markDirty(dateKey);
|
|
529
|
+
deps.persistence.scheduleSave();
|
|
530
|
+
}
|
|
458
531
|
};
|
|
459
532
|
const markForceRescan = (sessionID) => {
|
|
460
533
|
forceRescanSessions.add(sessionID);
|