@leo000001/opencode-quota-sidebar 3.0.9 → 4.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/CHANGELOG.md +0 -1
- package/README.md +157 -42
- package/README.zh-CN.md +157 -42
- package/SECURITY.md +1 -1
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +354 -0
- package/dist/cli_render.d.ts +17 -0
- package/dist/cli_render.js +292 -0
- package/dist/events.d.ts +1 -1
- package/dist/events.js +2 -2
- package/dist/format.d.ts +4 -0
- package/dist/format.js +302 -41
- package/dist/history_messages.d.ts +8 -0
- package/dist/history_messages.js +157 -0
- package/dist/history_usage.d.ts +93 -0
- package/dist/history_usage.js +251 -0
- package/dist/index.js +29 -4
- package/dist/period.d.ts +29 -1
- package/dist/period.js +187 -9
- package/dist/provider_catalog.d.ts +8 -0
- package/dist/provider_catalog.js +68 -0
- package/dist/providers/core/anthropic.d.ts +1 -1
- package/dist/providers/core/anthropic.js +69 -45
- package/dist/providers/core/openai.js +101 -8
- package/dist/providers/index.d.ts +1 -2
- package/dist/providers/index.js +1 -3
- package/dist/quota.d.ts +4 -2
- package/dist/quota.js +18 -21
- package/dist/quota_render.d.ts +1 -1
- package/dist/quota_render.js +23 -24
- package/dist/quota_service.d.ts +1 -0
- package/dist/quota_service.js +151 -19
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +4 -4
- package/dist/storage_dates.d.ts +1 -1
- package/dist/storage_dates.js +8 -5
- package/dist/storage_parse.js +23 -1
- package/dist/supported_quota.d.ts +4 -0
- package/dist/supported_quota.js +36 -0
- package/dist/title.js +18 -8
- package/dist/tools.d.ts +14 -3
- package/dist/tools.js +54 -2
- package/dist/tui.tsx +17 -6
- package/dist/tui_helpers.js +11 -6
- package/dist/types.d.ts +8 -0
- package/dist/usage.d.ts +18 -0
- package/dist/usage.js +93 -9
- package/dist/usage_service.d.ts +4 -1
- package/dist/usage_service.js +193 -189
- package/package.json +4 -1
- package/quota-sidebar.config.example.json +36 -45
- package/dist/providers/third_party/xyai.d.ts +0 -2
- package/dist/providers/third_party/xyai.js +0 -348
package/dist/quota_service.js
CHANGED
|
@@ -3,15 +3,27 @@ import { TtlValueCache } from './cache.js';
|
|
|
3
3
|
import { isRecord, swallow } from './helpers.js';
|
|
4
4
|
import { listDefaultQuotaProviderIDs, loadAuthMap, quotaSort } from './quota.js';
|
|
5
5
|
export function createQuotaService(deps) {
|
|
6
|
-
const ERROR_CACHE_TTL_MS =
|
|
6
|
+
const ERROR_CACHE_TTL_MS = 15_000;
|
|
7
7
|
const ZERO_QUOTA_CACHE_TTL_MS = 15_000;
|
|
8
|
+
const CRITICAL_QUOTA_CACHE_TTL_MS = 20_000;
|
|
8
9
|
const LOW_QUOTA_CACHE_TTL_MS = 30_000;
|
|
10
|
+
const MODERATE_QUOTA_CACHE_TTL_MS = 45_000;
|
|
9
11
|
const SOON_RESET_CACHE_TTL_MS = 15_000;
|
|
10
12
|
const SOON_RESET_WINDOW_MS = 2 * 60 * 1000;
|
|
13
|
+
const MIN_STALE_MAX_AGE_MS = 5 * 60 * 1000;
|
|
14
|
+
const MAX_STALE_MAX_AGE_MS = 15 * 60 * 1000;
|
|
15
|
+
const RESET_PASSED_STALE_GRACE_MS = 60_000;
|
|
11
16
|
const authCache = new TtlValueCache();
|
|
12
17
|
const providerOptionsCache = new TtlValueCache();
|
|
13
18
|
const inFlight = new Map();
|
|
19
|
+
const invalidatedCacheKeys = new Map();
|
|
20
|
+
const latestFetchGeneration = new Map();
|
|
21
|
+
let requestGeneration = 0;
|
|
14
22
|
let lastSuccessfulProviderOptionsMap = {};
|
|
23
|
+
const nextRequestGeneration = () => {
|
|
24
|
+
requestGeneration += 1;
|
|
25
|
+
return requestGeneration;
|
|
26
|
+
};
|
|
15
27
|
const authFingerprint = (auth) => {
|
|
16
28
|
if (!auth || typeof auth !== 'object')
|
|
17
29
|
return undefined;
|
|
@@ -78,7 +90,8 @@ export function createQuotaService(deps) {
|
|
|
78
90
|
})
|
|
79
91
|
.catch(swallow('getProviderOptionsMap:providerList'));
|
|
80
92
|
}
|
|
81
|
-
const data = isRecord(response) &&
|
|
93
|
+
const data = isRecord(response) &&
|
|
94
|
+
Object.prototype.hasOwnProperty.call(response, 'data')
|
|
82
95
|
? response.data
|
|
83
96
|
: undefined;
|
|
84
97
|
if (!response || data === undefined) {
|
|
@@ -89,7 +102,8 @@ export function createQuotaService(deps) {
|
|
|
89
102
|
throwOnError: true,
|
|
90
103
|
})
|
|
91
104
|
.catch(swallow('getProviderOptionsMap:providerListNoDataFallback'));
|
|
92
|
-
const fallbackData = isRecord(response) &&
|
|
105
|
+
const fallbackData = isRecord(response) &&
|
|
106
|
+
Object.prototype.hasOwnProperty.call(response, 'data')
|
|
93
107
|
? response.data
|
|
94
108
|
: undefined;
|
|
95
109
|
const fallbackRecord = isRecord(fallbackData) ? fallbackData : undefined;
|
|
@@ -110,7 +124,9 @@ export function createQuotaService(deps) {
|
|
|
110
124
|
const key = record.key;
|
|
111
125
|
if (typeof id !== 'string')
|
|
112
126
|
return acc;
|
|
113
|
-
if (!options ||
|
|
127
|
+
if (!options ||
|
|
128
|
+
typeof options !== 'object' ||
|
|
129
|
+
Array.isArray(options)) {
|
|
114
130
|
acc[id] =
|
|
115
131
|
typeof key === 'string' && key ? { apiKey: key } : {};
|
|
116
132
|
return acc;
|
|
@@ -118,7 +134,9 @@ export function createQuotaService(deps) {
|
|
|
118
134
|
const optionsRecord = options;
|
|
119
135
|
acc[id] = {
|
|
120
136
|
...optionsRecord,
|
|
121
|
-
...(typeof key === 'string' &&
|
|
137
|
+
...(typeof key === 'string' &&
|
|
138
|
+
key &&
|
|
139
|
+
optionsRecord.apiKey === undefined
|
|
122
140
|
? { apiKey: key }
|
|
123
141
|
: {}),
|
|
124
142
|
};
|
|
@@ -149,7 +167,8 @@ export function createQuotaService(deps) {
|
|
|
149
167
|
throwOnError: true,
|
|
150
168
|
})
|
|
151
169
|
.catch(swallow('getProviderOptionsMap:providerListFallback'));
|
|
152
|
-
const fallbackData = isRecord(response) &&
|
|
170
|
+
const fallbackData = isRecord(response) &&
|
|
171
|
+
Object.prototype.hasOwnProperty.call(response, 'data')
|
|
153
172
|
? response.data
|
|
154
173
|
: undefined;
|
|
155
174
|
const fallbackRecord = isRecord(fallbackData) ? fallbackData : undefined;
|
|
@@ -170,14 +189,18 @@ export function createQuotaService(deps) {
|
|
|
170
189
|
const key = record.key;
|
|
171
190
|
if (typeof id !== 'string')
|
|
172
191
|
return acc;
|
|
173
|
-
if (!options ||
|
|
192
|
+
if (!options ||
|
|
193
|
+
typeof options !== 'object' ||
|
|
194
|
+
Array.isArray(options)) {
|
|
174
195
|
acc[id] = typeof key === 'string' && key ? { apiKey: key } : {};
|
|
175
196
|
return acc;
|
|
176
197
|
}
|
|
177
198
|
const optionsRecord = options;
|
|
178
199
|
acc[id] = {
|
|
179
200
|
...optionsRecord,
|
|
180
|
-
...(typeof key === 'string' &&
|
|
201
|
+
...(typeof key === 'string' &&
|
|
202
|
+
key &&
|
|
203
|
+
optionsRecord.apiKey === undefined
|
|
181
204
|
? { apiKey: key }
|
|
182
205
|
: {}),
|
|
183
206
|
};
|
|
@@ -214,7 +237,9 @@ export function createQuotaService(deps) {
|
|
|
214
237
|
const optionsRecord = options;
|
|
215
238
|
acc[id] = {
|
|
216
239
|
...optionsRecord,
|
|
217
|
-
...(typeof key === 'string' &&
|
|
240
|
+
...(typeof key === 'string' &&
|
|
241
|
+
key &&
|
|
242
|
+
optionsRecord.apiKey === undefined
|
|
218
243
|
? { apiKey: key }
|
|
219
244
|
: {}),
|
|
220
245
|
};
|
|
@@ -283,8 +308,59 @@ export function createQuotaService(deps) {
|
|
|
283
308
|
}
|
|
284
309
|
return values;
|
|
285
310
|
};
|
|
311
|
+
const staleMaxAgeMs = () => Math.min(MAX_STALE_MAX_AGE_MS, Math.max(MIN_STALE_MAX_AGE_MS, deps.config.quota.refreshMs * 2));
|
|
312
|
+
const staleReason = (snapshot) => {
|
|
313
|
+
if (snapshot.status !== 'error')
|
|
314
|
+
return undefined;
|
|
315
|
+
const note = snapshot.note?.trim();
|
|
316
|
+
if (!note)
|
|
317
|
+
return undefined;
|
|
318
|
+
const normalized = note.toLowerCase();
|
|
319
|
+
if (normalized === 'timeout') {
|
|
320
|
+
return { kind: 'timeout', text: note };
|
|
321
|
+
}
|
|
322
|
+
if (normalized.includes('network request failed')) {
|
|
323
|
+
return { kind: 'network', text: note };
|
|
324
|
+
}
|
|
325
|
+
if (/^http\s+5\d\d\b/i.test(note)) {
|
|
326
|
+
return { kind: 'http_5xx', text: note };
|
|
327
|
+
}
|
|
328
|
+
if (/^http\s+(408|429)\b/i.test(note)) {
|
|
329
|
+
return { kind: 'http_transient', text: note };
|
|
330
|
+
}
|
|
331
|
+
if (normalized.includes('invalid response') ||
|
|
332
|
+
normalized.includes('missing quota fields')) {
|
|
333
|
+
return { kind: 'invalid_response', text: note };
|
|
334
|
+
}
|
|
335
|
+
return undefined;
|
|
336
|
+
};
|
|
337
|
+
const canReuseStaleQuota = (snapshot, now = Date.now()) => {
|
|
338
|
+
if (snapshot.status !== 'ok')
|
|
339
|
+
return false;
|
|
340
|
+
if (now - snapshot.checkedAt > staleMaxAgeMs())
|
|
341
|
+
return false;
|
|
342
|
+
const resetTimes = snapshotResetTimes(snapshot);
|
|
343
|
+
if (resetTimes.some((resetAt) => resetAt + RESET_PASSED_STALE_GRACE_MS <= now)) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
};
|
|
348
|
+
const withStaleQuota = (snapshot, failure, now = Date.now()) => {
|
|
349
|
+
const reason = staleReason(failure);
|
|
350
|
+
return {
|
|
351
|
+
...snapshot,
|
|
352
|
+
stale: {
|
|
353
|
+
staleAt: now,
|
|
354
|
+
staleReason: reason?.text || failure.note || 'error',
|
|
355
|
+
staleReasonKind: reason?.kind || 'unknown',
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
};
|
|
286
359
|
const effectiveQuotaCacheTtl = (snapshot, now = Date.now()) => {
|
|
287
360
|
let ttlMs = deps.config.quota.refreshMs;
|
|
361
|
+
if (snapshot.stale) {
|
|
362
|
+
ttlMs = Math.min(ttlMs, ERROR_CACHE_TTL_MS);
|
|
363
|
+
}
|
|
288
364
|
if (snapshot.status !== 'ok') {
|
|
289
365
|
ttlMs = Math.min(ttlMs, ERROR_CACHE_TTL_MS);
|
|
290
366
|
}
|
|
@@ -292,9 +368,15 @@ export function createQuotaService(deps) {
|
|
|
292
368
|
if (remainingPercents.some((value) => value <= 0)) {
|
|
293
369
|
ttlMs = Math.min(ttlMs, ZERO_QUOTA_CACHE_TTL_MS);
|
|
294
370
|
}
|
|
295
|
-
else if (remainingPercents.some((value) => value <=
|
|
371
|
+
else if (remainingPercents.some((value) => value <= 5)) {
|
|
372
|
+
ttlMs = Math.min(ttlMs, CRITICAL_QUOTA_CACHE_TTL_MS);
|
|
373
|
+
}
|
|
374
|
+
else if (remainingPercents.some((value) => value <= 15)) {
|
|
296
375
|
ttlMs = Math.min(ttlMs, LOW_QUOTA_CACHE_TTL_MS);
|
|
297
376
|
}
|
|
377
|
+
else if (remainingPercents.some((value) => value <= 30)) {
|
|
378
|
+
ttlMs = Math.min(ttlMs, MODERATE_QUOTA_CACHE_TTL_MS);
|
|
379
|
+
}
|
|
298
380
|
const resetTimes = snapshotResetTimes(snapshot);
|
|
299
381
|
if (resetTimes.some((resetAt) => resetAt <= now))
|
|
300
382
|
return 0;
|
|
@@ -391,18 +473,29 @@ export function createQuotaService(deps) {
|
|
|
391
473
|
const fetchSnapshot = (providerID, providerOptions) => {
|
|
392
474
|
const baseKey = deps.quotaRuntime.quotaCacheKey(providerID, providerOptions);
|
|
393
475
|
const cacheKey = `${baseKey}#${authScopeFor(providerID, providerOptions)}`;
|
|
476
|
+
const normalizedProviderID = deps.quotaRuntime.normalizeProviderID(providerID);
|
|
394
477
|
const cached = deps.state.quotaCache[cacheKey];
|
|
395
478
|
const now = Date.now();
|
|
479
|
+
const invalidatedAt = invalidatedCacheKeys.get(cacheKey);
|
|
396
480
|
const cacheTtl = cached ? effectiveQuotaCacheTtl(cached, now) : 0;
|
|
397
|
-
|
|
481
|
+
const freshnessAt = cached?.stale?.staleAt ?? cached?.checkedAt ?? 0;
|
|
482
|
+
if (cached &&
|
|
483
|
+
invalidatedAt === undefined &&
|
|
484
|
+
cacheTtl > 0 &&
|
|
485
|
+
now - freshnessAt <= cacheTtl) {
|
|
398
486
|
if (isValidQuotaCache(cached))
|
|
399
487
|
return Promise.resolve(cached);
|
|
400
488
|
delete deps.state.quotaCache[cacheKey];
|
|
401
489
|
cacheChanged = true;
|
|
402
490
|
}
|
|
403
491
|
const existing = inFlight.get(cacheKey);
|
|
404
|
-
if (existing
|
|
405
|
-
|
|
492
|
+
if (existing &&
|
|
493
|
+
(invalidatedAt === undefined || existing.generation >= invalidatedAt)) {
|
|
494
|
+
return existing.promise;
|
|
495
|
+
}
|
|
496
|
+
if (invalidatedAt !== undefined)
|
|
497
|
+
invalidatedCacheKeys.delete(cacheKey);
|
|
498
|
+
const generation = nextRequestGeneration();
|
|
406
499
|
const promise = deps.quotaRuntime
|
|
407
500
|
.fetchQuotaSnapshot(providerID, authMap, deps.config, async (id, next) => {
|
|
408
501
|
await deps.client.auth
|
|
@@ -421,16 +514,36 @@ export function createQuotaService(deps) {
|
|
|
421
514
|
.then((latest) => {
|
|
422
515
|
if (!latest)
|
|
423
516
|
return undefined;
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
517
|
+
const cachedOk = cached &&
|
|
518
|
+
cached.status === 'ok' &&
|
|
519
|
+
canReuseStaleQuota(cached, Date.now())
|
|
520
|
+
? cached
|
|
521
|
+
: undefined;
|
|
522
|
+
const next = cachedOk && staleReason(latest)
|
|
523
|
+
? withStaleQuota(cachedOk, latest, Date.now())
|
|
524
|
+
: latest.stale
|
|
525
|
+
? { ...latest, stale: undefined }
|
|
526
|
+
: latest;
|
|
527
|
+
const invalidatedGeneration = invalidatedCacheKeys.get(cacheKey) || 0;
|
|
528
|
+
if (generation >= (latestFetchGeneration.get(cacheKey) || 0) &&
|
|
529
|
+
generation >= invalidatedGeneration) {
|
|
530
|
+
deps.state.quotaCache[cacheKey] = next;
|
|
531
|
+
cacheChanged = true;
|
|
532
|
+
}
|
|
533
|
+
return next;
|
|
427
534
|
})
|
|
428
535
|
.finally(() => {
|
|
429
|
-
if (inFlight.get(cacheKey) === promise) {
|
|
536
|
+
if (inFlight.get(cacheKey)?.promise === promise) {
|
|
430
537
|
inFlight.delete(cacheKey);
|
|
431
538
|
}
|
|
432
539
|
});
|
|
433
|
-
|
|
540
|
+
latestFetchGeneration.set(cacheKey, generation);
|
|
541
|
+
inFlight.set(cacheKey, {
|
|
542
|
+
generation,
|
|
543
|
+
providerID,
|
|
544
|
+
normalizedProviderID,
|
|
545
|
+
promise,
|
|
546
|
+
});
|
|
434
547
|
return promise;
|
|
435
548
|
};
|
|
436
549
|
const fetched = await Promise.all(dedupedCandidates.map(({ providerID, providerOptions }) => fetchSnapshot(providerID, providerOptions)));
|
|
@@ -440,5 +553,24 @@ export function createQuotaService(deps) {
|
|
|
440
553
|
deps.scheduleSave();
|
|
441
554
|
return snapshots;
|
|
442
555
|
};
|
|
443
|
-
|
|
556
|
+
const invalidateForProvider = (providerID) => {
|
|
557
|
+
const normalized = deps.quotaRuntime.normalizeProviderID(providerID);
|
|
558
|
+
for (const [cacheKey, snapshot] of Object.entries(deps.state.quotaCache)) {
|
|
559
|
+
if (snapshot.providerID === providerID) {
|
|
560
|
+
invalidatedCacheKeys.set(cacheKey, nextRequestGeneration());
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (deps.quotaRuntime.normalizeProviderID(snapshot.providerID) ===
|
|
564
|
+
normalized) {
|
|
565
|
+
invalidatedCacheKeys.set(cacheKey, nextRequestGeneration());
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
for (const [cacheKey, entry] of inFlight.entries()) {
|
|
569
|
+
if (entry.providerID === providerID ||
|
|
570
|
+
entry.normalizedProviderID === normalized) {
|
|
571
|
+
invalidatedCacheKeys.set(cacheKey, nextRequestGeneration());
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
return { getQuotaSnapshots, invalidateForProvider };
|
|
444
576
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare function evictOldSessions(state: QuotaSidebarState, retentionDays
|
|
|
26
26
|
* M9 fix: scan from in-memory state first, only read disk for date keys
|
|
27
27
|
* not represented in memory.
|
|
28
28
|
*/
|
|
29
|
-
export declare function scanSessionsByCreatedRange(statePath: string, startAt: number, endAt?: number, memoryState?: QuotaSidebarState): Promise<{
|
|
29
|
+
export declare function scanSessionsByCreatedRange(statePath: string, startAt: number, endAt?: number, memoryState?: QuotaSidebarState, retentionDays?: number): Promise<{
|
|
30
30
|
sessionID: string;
|
|
31
31
|
dateKey: string;
|
|
32
32
|
state: SessionState;
|
package/dist/storage.js
CHANGED
|
@@ -38,13 +38,13 @@ export const defaultConfig = {
|
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
40
|
quota: {
|
|
41
|
-
refreshMs:
|
|
41
|
+
refreshMs: 1 * 60 * 1000,
|
|
42
42
|
includeOpenAI: true,
|
|
43
43
|
includeCopilot: true,
|
|
44
44
|
includeAnthropic: true,
|
|
45
45
|
providers: {},
|
|
46
46
|
refreshAccessToken: false,
|
|
47
|
-
requestTimeoutMs:
|
|
47
|
+
requestTimeoutMs: 12_000,
|
|
48
48
|
},
|
|
49
49
|
toast: {
|
|
50
50
|
durationMs: 12_000,
|
|
@@ -363,10 +363,10 @@ export function evictOldSessions(state, retentionDays) {
|
|
|
363
363
|
* M9 fix: scan from in-memory state first, only read disk for date keys
|
|
364
364
|
* not represented in memory.
|
|
365
365
|
*/
|
|
366
|
-
export async function scanSessionsByCreatedRange(statePath, startAt, endAt = Date.now(), memoryState) {
|
|
366
|
+
export async function scanSessionsByCreatedRange(statePath, startAt, endAt = Date.now(), memoryState, retentionDays = 730) {
|
|
367
367
|
const rootPath = chunkRootPathFromStateFile(statePath);
|
|
368
368
|
const deletedSessionIDs = new Set(Object.keys(memoryState?.deletedSessionDateMap || {}));
|
|
369
|
-
const dateKeys = dateKeysInRange(startAt, endAt);
|
|
369
|
+
const dateKeys = dateKeysInRange(startAt, endAt, retentionDays + 1);
|
|
370
370
|
if (!dateKeys.length) {
|
|
371
371
|
return [];
|
|
372
372
|
}
|
package/dist/storage_dates.d.ts
CHANGED
|
@@ -6,4 +6,4 @@ export declare function isDateKey(value: string): boolean;
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function dateKeyFromTimestamp(timestampMs: number): string;
|
|
8
8
|
export declare function dateStartFromKey(dateKey: string): number;
|
|
9
|
-
export declare function dateKeysInRange(startAt: number, endAt: number): string[];
|
|
9
|
+
export declare function dateKeysInRange(startAt: number, endAt: number, maxDays?: number): string[];
|
package/dist/storage_dates.js
CHANGED
|
@@ -65,9 +65,12 @@ export function dateStartFromKey(dateKey) {
|
|
|
65
65
|
const [yearText, monthText, dayText] = dateKey.split('-');
|
|
66
66
|
return new Date(Number(yearText), Number(monthText) - 1, Number(dayText)).getTime();
|
|
67
67
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
/**
|
|
69
|
+
* M7 fix: cap iteration at 731 calendar dates so a rolling 730-day retention
|
|
70
|
+
* window still covers both boundary days.
|
|
71
|
+
*/
|
|
72
|
+
const DEFAULT_MAX_DATE_RANGE_DAYS = 731;
|
|
73
|
+
export function dateKeysInRange(startAt, endAt, maxDays = DEFAULT_MAX_DATE_RANGE_DAYS) {
|
|
71
74
|
const startDate = new Date(startAt);
|
|
72
75
|
if (Number.isNaN(startDate.getTime()))
|
|
73
76
|
return [];
|
|
@@ -77,9 +80,9 @@ export function dateKeysInRange(startAt, endAt) {
|
|
|
77
80
|
const cursor = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
|
|
78
81
|
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
|
|
79
82
|
const keys = [];
|
|
83
|
+
const safeMaxDays = Math.max(1, Math.floor(maxDays));
|
|
80
84
|
let iterations = 0;
|
|
81
|
-
while (cursor.getTime() <= endDay.getTime() &&
|
|
82
|
-
iterations < MAX_DATE_RANGE_DAYS) {
|
|
85
|
+
while (cursor.getTime() <= endDay.getTime() && iterations < safeMaxDays) {
|
|
83
86
|
keys.push(dateKeyFromTimestamp(cursor.getTime()));
|
|
84
87
|
cursor.setDate(cursor.getDate() + 1);
|
|
85
88
|
iterations++;
|
package/dist/storage_parse.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { asNumber, isRecord } from './helpers.js';
|
|
2
|
+
import { isSupportedQuotaSnapshot } from './supported_quota.js';
|
|
2
3
|
import { normalizeTimestampMs } from './storage_dates.js';
|
|
3
4
|
function parseSessionTitleState(value) {
|
|
4
5
|
if (!isRecord(value))
|
|
@@ -152,7 +153,26 @@ function parseQuotaSnapshot(value) {
|
|
|
152
153
|
}))
|
|
153
154
|
.filter((window) => window.label || window.remainingPercent !== undefined)
|
|
154
155
|
: undefined;
|
|
155
|
-
|
|
156
|
+
const stale = isRecord(value.stale)
|
|
157
|
+
? (() => {
|
|
158
|
+
const staleReasonKind = value.stale.staleReasonKind === 'timeout' ||
|
|
159
|
+
value.stale.staleReasonKind === 'network' ||
|
|
160
|
+
value.stale.staleReasonKind === 'http_5xx' ||
|
|
161
|
+
value.stale.staleReasonKind === 'http_transient' ||
|
|
162
|
+
value.stale.staleReasonKind === 'invalid_response' ||
|
|
163
|
+
value.stale.staleReasonKind === 'unknown'
|
|
164
|
+
? value.stale.staleReasonKind
|
|
165
|
+
: 'unknown';
|
|
166
|
+
return {
|
|
167
|
+
staleAt: typeof value.stale.staleAt === 'number' ? value.stale.staleAt : 0,
|
|
168
|
+
staleReason: typeof value.stale.staleReason === 'string'
|
|
169
|
+
? value.stale.staleReason
|
|
170
|
+
: '',
|
|
171
|
+
staleReasonKind,
|
|
172
|
+
};
|
|
173
|
+
})()
|
|
174
|
+
: undefined;
|
|
175
|
+
const parsed = {
|
|
156
176
|
providerID: typeof value.providerID === 'string' ? value.providerID : label,
|
|
157
177
|
adapterID,
|
|
158
178
|
label,
|
|
@@ -169,7 +189,9 @@ function parseQuotaSnapshot(value) {
|
|
|
169
189
|
balance,
|
|
170
190
|
note: typeof value.note === 'string' ? value.note : undefined,
|
|
171
191
|
windows,
|
|
192
|
+
stale: stale && stale.staleAt > 0 && stale.staleReason ? stale : undefined,
|
|
172
193
|
};
|
|
194
|
+
return isSupportedQuotaSnapshot(parsed) ? parsed : undefined;
|
|
173
195
|
}
|
|
174
196
|
function parseQuotaSnapshots(value) {
|
|
175
197
|
if (!Array.isArray(value))
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { QuotaSnapshot } from './types.js';
|
|
2
|
+
export declare function isSupportedQuotaProviderID(providerID: string): boolean;
|
|
3
|
+
export declare function isSupportedQuotaSnapshot(quota: Pick<QuotaSnapshot, 'providerID' | 'adapterID'>): boolean;
|
|
4
|
+
export declare function isSupportedQuotaTitleLabel(label: string): boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const SUPPORTED_QUOTA_PROVIDER_IDS = new Set([
|
|
2
|
+
'openai',
|
|
3
|
+
'github-copilot',
|
|
4
|
+
'anthropic',
|
|
5
|
+
'kimi-for-coding',
|
|
6
|
+
'zhipuai-coding-plan',
|
|
7
|
+
'minimax-cn-coding-plan',
|
|
8
|
+
'rightcode',
|
|
9
|
+
]);
|
|
10
|
+
const SUPPORTED_QUOTA_TITLE_LABELS = new Set([
|
|
11
|
+
'OAI',
|
|
12
|
+
'Cop',
|
|
13
|
+
'Ant',
|
|
14
|
+
'Kimi',
|
|
15
|
+
'Zhipu',
|
|
16
|
+
'MiniMax',
|
|
17
|
+
'RC',
|
|
18
|
+
]);
|
|
19
|
+
export function isSupportedQuotaProviderID(providerID) {
|
|
20
|
+
if (providerID.startsWith('github-copilot'))
|
|
21
|
+
return true;
|
|
22
|
+
if (providerID.startsWith('rightcode-'))
|
|
23
|
+
return true;
|
|
24
|
+
return SUPPORTED_QUOTA_PROVIDER_IDS.has(providerID);
|
|
25
|
+
}
|
|
26
|
+
export function isSupportedQuotaSnapshot(quota) {
|
|
27
|
+
if (typeof quota.adapterID === 'string' && quota.adapterID) {
|
|
28
|
+
return isSupportedQuotaProviderID(quota.adapterID);
|
|
29
|
+
}
|
|
30
|
+
return isSupportedQuotaProviderID(quota.providerID);
|
|
31
|
+
}
|
|
32
|
+
export function isSupportedQuotaTitleLabel(label) {
|
|
33
|
+
if (SUPPORTED_QUOTA_TITLE_LABELS.has(label))
|
|
34
|
+
return true;
|
|
35
|
+
return /^RC-[^\s]+$/.test(label);
|
|
36
|
+
}
|
package/dist/title.js
CHANGED
|
@@ -59,28 +59,38 @@ function isQuotaDecoratedDetail(line) {
|
|
|
59
59
|
return false;
|
|
60
60
|
const resetValue = '[-:\\dhmD]+';
|
|
61
61
|
const compactResetValue = '\\d[\\d:.\\-hmD]*';
|
|
62
|
-
|
|
62
|
+
const legacyProvider = '[A-Z][A-Z0-9-]{1,15}';
|
|
63
|
+
if (/^(OAI|Cop|Ant|Kimi|RC(?:-[^\s]+)?)(?:\s+(?:\?|unsupported|unavailable|error|stale|St|(?:\d+h|D|W|M)\d{1,3}|(?:Sk5h|SkW)\d{1,3}|D[\d.,]+\/[\d.,]+|B(?:[¥$-])?[\d.,]+))+$/i.test(line)) {
|
|
63
64
|
return true;
|
|
64
65
|
}
|
|
65
|
-
if (/^(OpenAI|Copilot|Anthropic|Kimi|
|
|
66
|
+
if (/^(OpenAI|Copilot|Anthropic|Kimi|RC(?:-[^\s]+)?)\s*$/.test(line)) {
|
|
66
67
|
return true;
|
|
67
68
|
}
|
|
68
|
-
if (new RegExp(`^(?:(?:Daily\\s+\\$[\\d.,]+\\/\\$[\\d.,]+|\\$[\\d.,]+\\/\\$[\\d.,]+)(?:\\s+(?:Rst|Exp\\+?)\\s+${resetValue})?|(?:\\d+[hdw]|Weekly|Monthly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?|Balance\\s+\\$[\\d.,]+|Remaining\\s
|
|
69
|
+
if (new RegExp(`^(?:(?:(?:Daily\\s+\\$[\\d.,]+\\/\\$[\\d.,]+|\\$[\\d.,]+\\/\\$[\\d.,]+)(?:\\s+(?:Rst|Exp\\+?)\\s+${resetValue})?|(?:\\d+[hdw]|Weekly|Monthly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?|Balance\\s+\\$[\\d.,]+|Remaining\\s+\\?)(?:\\s+stale)?|(?:error|unsupported|unavailable|stale))$`).test(line)) {
|
|
69
70
|
return true;
|
|
70
71
|
}
|
|
71
|
-
if (new RegExp(`^(?:
|
|
72
|
+
if (new RegExp(`^(?:Spark\\s+(?:5h|Weekly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?)(?:\\s+stale)?$`).test(line)) {
|
|
72
73
|
return true;
|
|
73
74
|
}
|
|
74
|
-
if (new RegExp(`^(
|
|
75
|
+
if (new RegExp(`^(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d|Sk5h|SkW)\\d{1,3}|St)(?:\\s+(?:R|E\\+?)${compactResetValue})?$`).test(line)) {
|
|
75
76
|
return true;
|
|
76
77
|
}
|
|
77
|
-
if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|
|
|
78
|
+
if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|RC(?:-[^\\s]+)?)(?:\\s+(?:(?:Daily\\s+\\$[\\d.,]+\\/\\$[\\d.,]+|\\$[\\d.,]+\\/\\$[\\d.,]+)(?:\\s+(?:Rst|Exp\\+?)\\s+${resetValue})?|(?:\\d+[hdw]|Weekly|Monthly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?)(?:\\s+stale)?|(?:error|unsupported|unavailable|stale))$`).test(line)) {
|
|
78
79
|
return true;
|
|
79
80
|
}
|
|
80
|
-
if (new RegExp(`^(
|
|
81
|
+
if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|RC(?:-[^\\s]+)?)(?:\\s+(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d|Sk5h|SkW)\\d{1,3}|St)(?:\\s+(?:R|E\\+?)${compactResetValue})?)$`).test(line)) {
|
|
81
82
|
return true;
|
|
82
83
|
}
|
|
83
|
-
if (new RegExp(`^(
|
|
84
|
+
if (new RegExp(`^(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d|Sk5h|SkW)\\d{1,3}|St|(?:R|E\\+?)${compactResetValue}))(?:\\s+(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d|Sk5h|SkW)\\d{1,3}|St|(?:R|E\\+?)${compactResetValue})))*$`).test(line)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|RC(?:-[^\\s]+)?)(?:\\s+(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d|Sk5h|SkW)\\d{1,3}|St|(?:R|E\\+?)${compactResetValue})))*$`).test(line)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (new RegExp(`^${legacyProvider}(?:\\s+(?:(?:Daily\\s+\\$[\\d.,]+\\/\\$[\\d.,]+|\\$[\\d.,]+\\/\\$[\\d.,]+)(?:\\s+(?:Rst|Exp\\+?)\\s+${resetValue})?|(?:\\d+[hdw]|Weekly|Monthly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?|(?:error|unsupported|unavailable|stale)))$`).test(line)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (new RegExp(`^${legacyProvider}(?:\\s+(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3}|St|(?:R|E\\+?)${compactResetValue}))*$`).test(line)) {
|
|
84
94
|
return true;
|
|
85
95
|
}
|
|
86
96
|
return false;
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
2
|
import type { QuotaSnapshot } from './types.js';
|
|
3
3
|
import type { UsageSummary } from './usage.js';
|
|
4
|
+
import type { HistoryPeriod } from './period.js';
|
|
5
|
+
import type { HistoryUsageResult } from './usage_service.js';
|
|
4
6
|
type ToolContext = {
|
|
5
7
|
sessionID: string;
|
|
6
8
|
};
|
|
@@ -19,6 +21,8 @@ export declare function createQuotaSidebarTools(deps: {
|
|
|
19
21
|
restoreSessionTitle?: (sessionID: string) => Promise<boolean>;
|
|
20
22
|
showToast: (period: 'session' | 'day' | 'week' | 'month' | 'toggle', message: string) => Promise<void>;
|
|
21
23
|
summarizeForTool: (period: 'session' | 'day' | 'week' | 'month', sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
|
|
24
|
+
summarizeHistoryForTool: (period: HistoryPeriod, since: string) => Promise<HistoryUsageResult>;
|
|
25
|
+
listCurrentProviderIDs?: () => Promise<Set<string>>;
|
|
22
26
|
getQuotaSnapshots: (providerIDs: string[], options?: {
|
|
23
27
|
allowDefault?: boolean;
|
|
24
28
|
}) => Promise<QuotaSnapshot[]>;
|
|
@@ -29,6 +33,9 @@ export declare function createQuotaSidebarTools(deps: {
|
|
|
29
33
|
showCost?: boolean;
|
|
30
34
|
width?: number;
|
|
31
35
|
}) => string;
|
|
36
|
+
renderHistoryMarkdownReport: (result: HistoryUsageResult, quotas: QuotaSnapshot[], options?: {
|
|
37
|
+
showCost?: boolean;
|
|
38
|
+
}) => string;
|
|
32
39
|
config: {
|
|
33
40
|
sidebar: {
|
|
34
41
|
showCost: boolean;
|
|
@@ -42,16 +49,20 @@ export declare function createQuotaSidebarTools(deps: {
|
|
|
42
49
|
description: string;
|
|
43
50
|
args: {
|
|
44
51
|
period: z.ZodOptional<z.ZodEnum<{
|
|
45
|
-
day: "day";
|
|
46
|
-
week: "week";
|
|
47
52
|
month: "month";
|
|
53
|
+
day: "day";
|
|
48
54
|
session: "session";
|
|
55
|
+
week: "week";
|
|
49
56
|
}>>;
|
|
57
|
+
since: z.ZodOptional<z.ZodString>;
|
|
58
|
+
last: z.ZodOptional<z.ZodNumber>;
|
|
50
59
|
toast: z.ZodOptional<z.ZodBoolean>;
|
|
51
60
|
includeChildren: z.ZodOptional<z.ZodBoolean>;
|
|
52
61
|
};
|
|
53
62
|
execute: (args: {
|
|
54
|
-
period?: "
|
|
63
|
+
period?: "month" | "day" | "session" | "week" | undefined;
|
|
64
|
+
since?: string | undefined;
|
|
65
|
+
last?: number | undefined;
|
|
55
66
|
toast?: boolean | undefined;
|
|
56
67
|
includeChildren?: boolean | undefined;
|
|
57
68
|
}, context: ToolContext) => Promise<string>;
|
package/dist/tools.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
|
+
import { sinceFromLast } from './period.js';
|
|
3
|
+
import { filterHistoryProvidersForDisplay, filterUsageProvidersForDisplay, } from './provider_catalog.js';
|
|
2
4
|
function tool(input) {
|
|
3
5
|
return input;
|
|
4
6
|
}
|
|
@@ -16,6 +18,16 @@ export function createQuotaSidebarTools(deps) {
|
|
|
16
18
|
description: 'Show usage and quota summary for session/day/week/month. Returns the full markdown report with totals, highlights, provider table, and subscription quota so callers can present the report directly to the user.',
|
|
17
19
|
args: {
|
|
18
20
|
period: z.enum(['session', 'day', 'week', 'month']).optional(),
|
|
21
|
+
since: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Historical start date: `YYYY-MM` or `YYYY-MM-DD`.'),
|
|
25
|
+
last: z
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.positive()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Relative history length. Examples: `period=day,last=7`, `period=week,last=8`, `period=month,last=6`.'),
|
|
19
31
|
toast: z.boolean().optional(),
|
|
20
32
|
includeChildren: z
|
|
21
33
|
.boolean()
|
|
@@ -23,11 +35,51 @@ export function createQuotaSidebarTools(deps) {
|
|
|
23
35
|
.describe('For period=session, include descendant subagent sessions in usage aggregation.'),
|
|
24
36
|
},
|
|
25
37
|
execute: async (args, context) => {
|
|
26
|
-
const period = args.period || 'session';
|
|
38
|
+
const period = args.period || (args.since || args.last ? 'month' : 'session');
|
|
39
|
+
const since = args.since?.trim();
|
|
40
|
+
const last = args.last;
|
|
41
|
+
if (since && last !== undefined) {
|
|
42
|
+
throw new Error('`since` and `last` cannot be used together');
|
|
43
|
+
}
|
|
44
|
+
if (period === 'session' && since) {
|
|
45
|
+
throw new Error('`since` is not supported when `period=session`');
|
|
46
|
+
}
|
|
47
|
+
if (period === 'session' && last !== undefined) {
|
|
48
|
+
throw new Error('`last` is not supported when `period=session`');
|
|
49
|
+
}
|
|
50
|
+
const resolvedSince = since ||
|
|
51
|
+
(period !== 'session' && last !== undefined
|
|
52
|
+
? sinceFromLast(period, last)
|
|
53
|
+
: undefined);
|
|
54
|
+
const allowedProviderIDs = await deps
|
|
55
|
+
.listCurrentProviderIDs?.()
|
|
56
|
+
.catch(() => new Set());
|
|
57
|
+
if (period !== 'session' && resolvedSince) {
|
|
58
|
+
const historyRaw = await deps.summarizeHistoryForTool(period, resolvedSince);
|
|
59
|
+
const history = allowedProviderIDs
|
|
60
|
+
? filterHistoryProvidersForDisplay(historyRaw, allowedProviderIDs)
|
|
61
|
+
: historyRaw;
|
|
62
|
+
const quotas = await deps.getQuotaSnapshots([], {
|
|
63
|
+
allowDefault: true,
|
|
64
|
+
});
|
|
65
|
+
const markdown = deps.renderHistoryMarkdownReport(history, quotas, {
|
|
66
|
+
showCost: deps.config.sidebar.showCost,
|
|
67
|
+
});
|
|
68
|
+
if (args.toast === true) {
|
|
69
|
+
await deps.showToast(period, deps.renderToastMessage(period, history.total, quotas, {
|
|
70
|
+
showCost: deps.config.sidebar.showCost,
|
|
71
|
+
width: Math.max(44, deps.config.sidebar.width + 18),
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
return markdown;
|
|
75
|
+
}
|
|
27
76
|
const includeChildren = period === 'session'
|
|
28
77
|
? (args.includeChildren ?? deps.config.sidebar.includeChildren)
|
|
29
78
|
: false;
|
|
30
|
-
const
|
|
79
|
+
const usageRaw = await deps.summarizeForTool(period, context.sessionID, includeChildren);
|
|
80
|
+
const usage = allowedProviderIDs
|
|
81
|
+
? filterUsageProvidersForDisplay(usageRaw, allowedProviderIDs)
|
|
82
|
+
: usageRaw;
|
|
31
83
|
// For quota_summary, always show all subscription quota balances,
|
|
32
84
|
// regardless of which providers were used in the session.
|
|
33
85
|
const quotas = await deps.getQuotaSnapshots([], { allowDefault: true });
|