@leo000001/opencode-quota-sidebar 2.0.6 → 2.0.8
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/title.js +19 -3
- package/dist/title_apply.js +7 -0
- package/dist/usage_service.js +112 -19
- package/package.json +1 -1
package/dist/title.js
CHANGED
|
@@ -28,6 +28,20 @@ function isCoreDecoratedDetail(line) {
|
|
|
28
28
|
}
|
|
29
29
|
return false;
|
|
30
30
|
}
|
|
31
|
+
function isQuotaDecoratedDetail(line) {
|
|
32
|
+
if (!line)
|
|
33
|
+
return false;
|
|
34
|
+
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|Buzz|RC(?:-[^\s]+)?)\s*$/.test(line)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (/^(?:(?:Daily\s+\$[\d.,]+\/\$[\d.,]+|\$[\d.,]+\/\$[\d.,]+)(?:\s+(?:Rst|Exp\+?)\s+[-:\d]+)?|(?:\d+[hdw]|Weekly|Monthly)\s+\d{1,3}%(?:\s+Rst\s+[-:\d]+)?|Balance\s+\$[\d.,]+|Remaining\s+\?|(?:error|unsupported|unavailable))$/.test(line)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|Buzz|RC(?:-[^\s]+)?)(?:\s+(?:(?:Daily\s+\$[\d.,]+\/\$[\d.,]+|\$[\d.,]+\/\$[\d.,]+)(?:\s+(?:Rst|Exp\+?)\s+[-:\d]+)?|(?:\d+[hdw]|Weekly|Monthly)\s+\d{1,3}%(?:\s+Rst\s+[-:\d]+)?|(?:error|unsupported|unavailable)))$/.test(line)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
31
45
|
function isSingleLineDecoratedPrefix(line) {
|
|
32
46
|
if (!line)
|
|
33
47
|
return false;
|
|
@@ -47,7 +61,9 @@ function isSingleLineDecoratedPrefix(line) {
|
|
|
47
61
|
return false;
|
|
48
62
|
}
|
|
49
63
|
function isSingleLineDetailPrefix(line) {
|
|
50
|
-
return isCoreDecoratedDetail(line) ||
|
|
64
|
+
return (isCoreDecoratedDetail(line) ||
|
|
65
|
+
isSingleLineDecoratedPrefix(line) ||
|
|
66
|
+
isQuotaDecoratedDetail(line));
|
|
51
67
|
}
|
|
52
68
|
function decoratedSingleLineBase(line) {
|
|
53
69
|
const parts = sanitizeTitleFragment(line)
|
|
@@ -72,7 +88,7 @@ export function normalizeBaseTitle(title) {
|
|
|
72
88
|
const lines = stripAnsi(safeTitle).split(/\r?\n/);
|
|
73
89
|
if (lines.length > 1) {
|
|
74
90
|
const detail = lines.slice(1).map((line) => sanitizeTitleFragment(line).trim());
|
|
75
|
-
if (detail.some((line) => isCoreDecoratedDetail(line))) {
|
|
91
|
+
if (detail.some((line) => isCoreDecoratedDetail(line) || isQuotaDecoratedDetail(line))) {
|
|
76
92
|
return sanitizeTitleFragment(firstLine) || 'Session';
|
|
77
93
|
}
|
|
78
94
|
}
|
|
@@ -122,5 +138,5 @@ export function looksDecorated(title) {
|
|
|
122
138
|
const detail = lines
|
|
123
139
|
.slice(1)
|
|
124
140
|
.map((line) => sanitizeTitleFragment(line).trim());
|
|
125
|
-
return detail.some((line) => isCoreDecoratedDetail(line));
|
|
141
|
+
return detail.some((line) => isCoreDecoratedDetail(line) || isQuotaDecoratedDetail(line));
|
|
126
142
|
}
|
package/dist/title_apply.js
CHANGED
|
@@ -33,6 +33,13 @@ export function createTitleApplicator(deps) {
|
|
|
33
33
|
if (canonicalizeTitle(currentTitle) !==
|
|
34
34
|
canonicalizeTitle(sessionState.lastAppliedTitle || '')) {
|
|
35
35
|
if (looksDecorated(currentTitle)) {
|
|
36
|
+
if (/\r?\n/.test(currentTitle)) {
|
|
37
|
+
const normalizedBase = normalizeBaseTitle(currentTitle);
|
|
38
|
+
if (sessionState.baseTitle !== normalizedBase) {
|
|
39
|
+
sessionState.baseTitle = normalizedBase;
|
|
40
|
+
stateMutated = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
36
43
|
// Ignore decorated echoes as base-title source.
|
|
37
44
|
// If we previously applied a decorated title, treat this as an
|
|
38
45
|
// equivalent echo (OpenCode may normalize whitespace) and keep
|
package/dist/usage_service.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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, scanAllSessions, updateSessionsInDayChunks, } from './storage.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',
|
|
@@ -218,18 +218,81 @@ export function createUsageService(deps) {
|
|
|
218
218
|
return undefined;
|
|
219
219
|
return decoded;
|
|
220
220
|
};
|
|
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)
|
|
221
|
+
const errorStatusCode = (value, seen = new Set()) => {
|
|
222
|
+
if (!isRecord(value) || seen.has(value))
|
|
230
223
|
return undefined;
|
|
231
|
-
|
|
232
|
-
|
|
224
|
+
seen.add(value);
|
|
225
|
+
const status = value.status;
|
|
226
|
+
if (typeof status === 'number' && Number.isFinite(status))
|
|
227
|
+
return status;
|
|
228
|
+
const statusCode = value.statusCode;
|
|
229
|
+
if (typeof statusCode === 'number' && Number.isFinite(statusCode)) {
|
|
230
|
+
return statusCode;
|
|
231
|
+
}
|
|
232
|
+
return (errorStatusCode(value.response, seen) ||
|
|
233
|
+
errorStatusCode(value.cause, seen) ||
|
|
234
|
+
errorStatusCode(value.error, seen));
|
|
235
|
+
};
|
|
236
|
+
const errorText = (value, seen = new Set()) => {
|
|
237
|
+
if (!value || seen.has(value))
|
|
238
|
+
return '';
|
|
239
|
+
if (typeof value === 'string')
|
|
240
|
+
return value;
|
|
241
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
242
|
+
return `${value}`;
|
|
243
|
+
if (value instanceof Error) {
|
|
244
|
+
seen.add(value);
|
|
245
|
+
return [
|
|
246
|
+
value.message,
|
|
247
|
+
errorText(value.cause, seen),
|
|
248
|
+
]
|
|
249
|
+
.filter(Boolean)
|
|
250
|
+
.join('\n');
|
|
251
|
+
}
|
|
252
|
+
if (!isRecord(value))
|
|
253
|
+
return '';
|
|
254
|
+
seen.add(value);
|
|
255
|
+
return [
|
|
256
|
+
typeof value.message === 'string' ? value.message : '',
|
|
257
|
+
typeof value.error === 'string' ? value.error : '',
|
|
258
|
+
typeof value.detail === 'string' ? value.detail : '',
|
|
259
|
+
typeof value.title === 'string' ? value.title : '',
|
|
260
|
+
errorText(value.response, seen),
|
|
261
|
+
errorText(value.data, seen),
|
|
262
|
+
errorText(value.cause, seen),
|
|
263
|
+
]
|
|
264
|
+
.filter(Boolean)
|
|
265
|
+
.join('\n');
|
|
266
|
+
};
|
|
267
|
+
const isMissingSessionError = (error) => {
|
|
268
|
+
const status = errorStatusCode(error);
|
|
269
|
+
if (status === 404 || status === 410)
|
|
270
|
+
return true;
|
|
271
|
+
const text = errorText(error).toLowerCase();
|
|
272
|
+
if (!text)
|
|
273
|
+
return false;
|
|
274
|
+
return (/\b(session|conversation)\b.*\b(not found|missing|deleted|does not exist)\b/.test(text) ||
|
|
275
|
+
/\b(not found|missing|deleted|does not exist)\b.*\b(session|conversation)\b/.test(text));
|
|
276
|
+
};
|
|
277
|
+
const loadSessionEntries = async (sessionID) => {
|
|
278
|
+
try {
|
|
279
|
+
const response = await deps.client.session.messages({
|
|
280
|
+
path: { id: sessionID },
|
|
281
|
+
query: { directory: deps.directory },
|
|
282
|
+
throwOnError: true,
|
|
283
|
+
});
|
|
284
|
+
const data = response.data;
|
|
285
|
+
const entries = decodeMessageEntries(data);
|
|
286
|
+
if (!entries)
|
|
287
|
+
return { status: 'error' };
|
|
288
|
+
return { status: 'ok', entries };
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
debugError(`loadSessionEntries ${sessionID}`, error);
|
|
292
|
+
return {
|
|
293
|
+
status: isMissingSessionError(error) ? 'missing' : 'error',
|
|
294
|
+
};
|
|
295
|
+
}
|
|
233
296
|
};
|
|
234
297
|
const persistSessionUsage = (sessionID, usage) => {
|
|
235
298
|
const sessionState = deps.state.sessions[sessionID];
|
|
@@ -262,7 +325,8 @@ export function createUsageService(deps) {
|
|
|
262
325
|
});
|
|
263
326
|
};
|
|
264
327
|
const summarizeSessionUsage = async (sessionID, generationAtStart, options) => {
|
|
265
|
-
const
|
|
328
|
+
const load = await loadSessionEntries(sessionID);
|
|
329
|
+
const entries = load.status === 'ok' ? load.entries : undefined;
|
|
266
330
|
const sessionState = deps.state.sessions[sessionID];
|
|
267
331
|
// If we can't load messages (transient API failure), fall back to cached
|
|
268
332
|
// usage if available and avoid mutating cursor/dirty state.
|
|
@@ -426,8 +490,8 @@ export function createUsageService(deps) {
|
|
|
426
490
|
};
|
|
427
491
|
if (sessions.length > 0) {
|
|
428
492
|
const fetched = await mapConcurrent(sessions, RANGE_USAGE_CONCURRENCY, async (session) => {
|
|
429
|
-
const
|
|
430
|
-
if (
|
|
493
|
+
const load = await loadSessionEntries(session.sessionID);
|
|
494
|
+
if (load.status !== 'ok') {
|
|
431
495
|
return {
|
|
432
496
|
sessionID: session.sessionID,
|
|
433
497
|
dateKey: session.dateKey,
|
|
@@ -436,16 +500,19 @@ export function createUsageService(deps) {
|
|
|
436
500
|
dirty: session.state.dirty === true,
|
|
437
501
|
computed: emptyUsageSummary(),
|
|
438
502
|
fullUsage: undefined,
|
|
439
|
-
loadFailed:
|
|
503
|
+
loadFailed: load.status === 'error',
|
|
504
|
+
missing: load.status === 'missing',
|
|
440
505
|
persist: false,
|
|
441
506
|
cursor: undefined,
|
|
442
507
|
};
|
|
443
508
|
}
|
|
509
|
+
const entries = load.entries;
|
|
444
510
|
const computed = summarizeMessagesInCompletedRange(entries, startAt, endAt, 0, {
|
|
445
511
|
calcApiCost: (message) => calcEquivalentApiCost(message, modelCostMap),
|
|
446
512
|
classifyCacheMode: (message) => classifyCacheMode(message, modelCostMap),
|
|
447
513
|
});
|
|
448
|
-
const shouldPersistFullUsage = !session.state.usage ||
|
|
514
|
+
const shouldPersistFullUsage = !session.state.usage ||
|
|
515
|
+
shouldRecomputeUsageCache(session.state.usage);
|
|
449
516
|
if (!shouldPersistFullUsage) {
|
|
450
517
|
return {
|
|
451
518
|
sessionID: session.sessionID,
|
|
@@ -456,6 +523,7 @@ export function createUsageService(deps) {
|
|
|
456
523
|
computed,
|
|
457
524
|
fullUsage: undefined,
|
|
458
525
|
loadFailed: false,
|
|
526
|
+
missing: false,
|
|
459
527
|
persist: false,
|
|
460
528
|
cursor: session.state.cursor,
|
|
461
529
|
};
|
|
@@ -473,10 +541,35 @@ export function createUsageService(deps) {
|
|
|
473
541
|
computed,
|
|
474
542
|
fullUsage,
|
|
475
543
|
loadFailed: false,
|
|
544
|
+
missing: false,
|
|
476
545
|
persist: true,
|
|
477
546
|
cursor,
|
|
478
547
|
};
|
|
479
548
|
});
|
|
549
|
+
const missingSessions = fetched.filter((item) => item.missing);
|
|
550
|
+
if (missingSessions.length > 0) {
|
|
551
|
+
let stateChanged = false;
|
|
552
|
+
for (const missing of missingSessions) {
|
|
553
|
+
deps.state.deletedSessionDateMap[missing.sessionID] = missing.dateKey;
|
|
554
|
+
delete deps.state.sessions[missing.sessionID];
|
|
555
|
+
delete deps.state.sessionDateMap[missing.sessionID];
|
|
556
|
+
deps.persistence.markDirty(missing.dateKey);
|
|
557
|
+
forgetSession(missing.sessionID);
|
|
558
|
+
stateChanged = true;
|
|
559
|
+
}
|
|
560
|
+
await Promise.all(missingSessions.map(async (missing) => {
|
|
561
|
+
const deletedFromChunk = await deleteSessionFromDayChunk(deps.statePath, missing.sessionID, missing.dateKey).catch((error) => {
|
|
562
|
+
swallow('deleteSessionFromDayChunk')(error);
|
|
563
|
+
return false;
|
|
564
|
+
});
|
|
565
|
+
if (!deletedFromChunk)
|
|
566
|
+
return;
|
|
567
|
+
delete deps.state.deletedSessionDateMap[missing.sessionID];
|
|
568
|
+
stateChanged = true;
|
|
569
|
+
}));
|
|
570
|
+
if (stateChanged)
|
|
571
|
+
deps.persistence.scheduleSave();
|
|
572
|
+
}
|
|
480
573
|
const failedLoads = fetched.filter((item) => {
|
|
481
574
|
if (!item.loadFailed)
|
|
482
575
|
return false;
|
|
@@ -493,7 +586,7 @@ export function createUsageService(deps) {
|
|
|
493
586
|
}
|
|
494
587
|
let dirty = false;
|
|
495
588
|
const diskOnlyUpdates = [];
|
|
496
|
-
for (const { sessionID, dateKey, computed, fullUsage, persist, cursor } of fetched) {
|
|
589
|
+
for (const { sessionID, dateKey, computed, fullUsage, persist, cursor, } of fetched) {
|
|
497
590
|
if (computed.assistantMessages > 0) {
|
|
498
591
|
computed.sessionCount = 1;
|
|
499
592
|
mergeUsage(usage, computed);
|