dsh-subscriptions 0.1.1 → 0.1.3
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 +115 -4
- package/icon.svg +28 -0
- package/lib/client/CursorCard.js +15 -11
- package/lib/client/ExternalUsageCards.d.ts +11 -1
- package/lib/client/ExternalUsageCards.js +28 -13
- package/lib/client/SubscriptionUsageBadge.d.ts +25 -4
- package/lib/client/SubscriptionUsageBadge.js +115 -28
- package/lib/client/SubscriptionsSection.d.ts +7 -2
- package/lib/client/SubscriptionsSection.js +9 -13
- package/lib/client/UsageBadgeDisplaySetting.js +19 -2
- package/lib/client/UsageMeter.d.ts +8 -0
- package/lib/client/UsageMeter.js +56 -0
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +1 -0
- package/lib/client/locales.d.ts +50 -0
- package/lib/client/locales.js +52 -2
- package/lib/client/subscription-card-styles.js +0 -5
- package/lib/client/usage-badge-preferences.d.ts +7 -2
- package/lib/client/usage-badge-preferences.js +3 -2
- package/lib/client/usage-color-preferences.d.ts +6 -0
- package/lib/client/usage-color-preferences.js +38 -0
- package/lib/client/usage-pace.d.ts +42 -0
- package/lib/client/usage-pace.js +71 -0
- package/lib/client.js +489 -132
- package/lib/client.js.map +1 -1
- package/lib/index.js +252 -35
- package/lib/providers/claude.js +7 -0
- package/lib/providers/codex.js +5 -0
- package/lib/providers/common.d.ts +10 -0
- package/lib/providers/cursor-usage.js +6 -2
- package/lib/providers/external-usage-controller.d.ts +1 -1
- package/lib/providers/external-usage-controller.js +8 -3
- package/lib/providers/external-usage.d.ts +1 -1
- package/lib/providers/external-usage.js +62 -2
- package/lib/providers/grok.js +3 -1
- package/lib/providers/minimax-usage.d.ts +24 -0
- package/lib/providers/minimax-usage.js +85 -0
- package/lib/providers/pool-usage.js +6 -4
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +7 -3
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
/** Usage readers for subscriptions whose model adapters are built into DSH. */
|
|
2
|
+
function numeric(value) {
|
|
3
|
+
const n = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
4
|
+
return typeof n === 'number' && Number.isFinite(n) ? n : undefined;
|
|
5
|
+
}
|
|
6
|
+
function kimiReset(value) {
|
|
7
|
+
for (const key of ['reset_at', 'resetAt', 'reset_time', 'resetTime']) {
|
|
8
|
+
const reset = resetTime(value[key]);
|
|
9
|
+
if (reset !== undefined)
|
|
10
|
+
return reset;
|
|
11
|
+
}
|
|
12
|
+
for (const key of ['reset_in', 'resetIn', 'ttl', 'window']) {
|
|
13
|
+
const seconds = numeric(value[key]);
|
|
14
|
+
if (seconds !== undefined && seconds > 0)
|
|
15
|
+
return Date.now() + seconds * 1000;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
2
19
|
function record(value) {
|
|
3
20
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
4
21
|
? value
|
|
@@ -45,6 +62,9 @@ export async function fetchOpenCodeGoUsage(apiKey, http = fetch, signal) {
|
|
|
45
62
|
kind,
|
|
46
63
|
...(scope === undefined ? {} : { scope }),
|
|
47
64
|
usedPercent,
|
|
65
|
+
// Go's rolling counter is first-use anchored, not per-request sliding.
|
|
66
|
+
...(field === 'rolling' ? { fixedWindow: true, windowDurationMs: 18_000_000 }
|
|
67
|
+
: field === 'weekly' ? { fixedWindow: true, windowDurationMs: 604_800_000 } : {}),
|
|
48
68
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
49
69
|
});
|
|
50
70
|
}
|
|
@@ -52,14 +72,53 @@ export async function fetchOpenCodeGoUsage(apiKey, http = fetch, signal) {
|
|
|
52
72
|
throw new Error('OpenCode Go usage response has no valid windows');
|
|
53
73
|
return { supported: true, windows, plan: 'OpenCode Go' };
|
|
54
74
|
}
|
|
55
|
-
/** Read
|
|
75
|
+
/** Read official CLI count windows and retain compatibility with older ratio pools. */
|
|
56
76
|
export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
57
77
|
const body = record(await getUsage('https://api.kimi.com/coding/v1/usages', apiKey, http, signal));
|
|
58
78
|
if (body === undefined)
|
|
59
79
|
throw new Error('Kimi Code usage response is not an object');
|
|
80
|
+
const modern = [];
|
|
81
|
+
const rows = [
|
|
82
|
+
...(record(body.usage) ? [{ detail: body.usage }] : []),
|
|
83
|
+
...(Array.isArray(body.limits) ? body.limits : []),
|
|
84
|
+
];
|
|
85
|
+
for (const raw of rows) {
|
|
86
|
+
const item = record(raw);
|
|
87
|
+
if (!item)
|
|
88
|
+
continue;
|
|
89
|
+
const detail = record(item.detail) ?? item;
|
|
90
|
+
const limit = numeric(detail.limit);
|
|
91
|
+
const used = numeric(detail.used) ?? (limit !== undefined && numeric(detail.remaining) !== undefined
|
|
92
|
+
? limit - numeric(detail.remaining) : undefined);
|
|
93
|
+
if (limit === undefined || limit <= 0 || used === undefined)
|
|
94
|
+
continue;
|
|
95
|
+
const usedPercent = percent(used / limit * 100);
|
|
96
|
+
if (usedPercent === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
const window = record(item.window) ?? item;
|
|
99
|
+
const duration = numeric(window.duration ?? item.duration ?? detail.duration);
|
|
100
|
+
const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? '').toUpperCase();
|
|
101
|
+
const multiplier = unit.includes('MINUTE') ? 60_000 : unit.includes('HOUR') ? 3_600_000
|
|
102
|
+
: unit.includes('DAY') ? 86_400_000 : unit.includes('SECOND') ? 1000 : undefined;
|
|
103
|
+
const durationMs = duration === undefined || duration <= 0 || multiplier === undefined
|
|
104
|
+
? undefined : duration * multiplier;
|
|
105
|
+
const kind = durationMs === 18_000_000 ? 'session' : durationMs === 604_800_000 ? 'weekly' : 'other';
|
|
106
|
+
const resetsAt = kimiReset(detail);
|
|
107
|
+
const scope = detail.name ?? detail.title ?? item.name ?? item.title;
|
|
108
|
+
modern.push({ kind, usedPercent,
|
|
109
|
+
...(typeof scope === 'string' ? { scope } : {}),
|
|
110
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
111
|
+
...(durationMs === undefined ? {} : { windowDurationMs: durationMs }),
|
|
112
|
+
// Legacy weekly plans reset as whole seven-day buckets; five-hour semantics
|
|
113
|
+
// are documented as rolling, so no fixed-window assertion is made there.
|
|
114
|
+
...(kind === 'weekly' ? { fixedWindow: true } : {}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (modern.length)
|
|
118
|
+
return { supported: true, windows: modern, plan: 'Kimi Code' };
|
|
60
119
|
const usages = record(body.usages);
|
|
61
120
|
if (usages === undefined)
|
|
62
|
-
throw new Error('Kimi Code usage response has no
|
|
121
|
+
throw new Error('Kimi Code usage response has no valid windows');
|
|
63
122
|
const windows = [];
|
|
64
123
|
for (const [field, kind, scope] of [
|
|
65
124
|
['limit_5h', 'session', undefined],
|
|
@@ -75,6 +134,7 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
|
75
134
|
kind,
|
|
76
135
|
...(scope === undefined ? {} : { scope }),
|
|
77
136
|
usedPercent: ratio * 100,
|
|
137
|
+
...(field === 'limit_7d' ? { fixedWindow: true, windowDurationMs: 604_800_000 } : {}),
|
|
78
138
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
79
139
|
});
|
|
80
140
|
}
|
package/lib/providers/grok.js
CHANGED
|
@@ -272,7 +272,9 @@ export async function fetchGrokUsage(session, fetchFn = hostFetch, signal) {
|
|
|
272
272
|
const usedPercent = typeof config.creditUsagePercent === 'number' && Number.isFinite(config.creditUsagePercent)
|
|
273
273
|
? config.creditUsagePercent
|
|
274
274
|
: 0;
|
|
275
|
-
|
|
275
|
+
const startsAt = grokResetsAt(config.currentPeriod?.start);
|
|
276
|
+
windows.push({ kind, usedPercent, ...resetsAt === undefined ? {} : { resetsAt },
|
|
277
|
+
...startsAt === undefined ? {} : { startsAt } });
|
|
276
278
|
}
|
|
277
279
|
else if (typeof config.monthlyLimit?.val === 'number' && config.monthlyLimit.val > 0) {
|
|
278
280
|
const used = typeof config.used?.val === 'number' ? config.used.val : 0;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ProviderUsage } from './common.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read MiniMax subscription quota (M Plan and Token Plan), not the balance API
|
|
4
|
+
* that `sk-api-*` pay-as-you-go keys use.
|
|
5
|
+
*
|
|
6
|
+
* Each `model_remains` row can yield an interval window and a weekly window.
|
|
7
|
+
* A window needs a finite share: the explicit `*_remaining_percent` (the weekly
|
|
8
|
+
* one scaled by `weekly_boost_permille`), else the legacy counts. Time-based
|
|
9
|
+
* plans report zero counts beside the percentage for standard models, so the
|
|
10
|
+
* percentage stands on its own and a zero total does not hide the window.
|
|
11
|
+
* Windows that cannot be a finite bar are omitted rather than approximated:
|
|
12
|
+
* status 3 (unlimited, or no quota in the plan), a weekly pool boosted past
|
|
13
|
+
* 100% remaining, a percentage outside 0-100, and a row with neither a
|
|
14
|
+
* percentage nor positive counts. Start and end times are kept only when both
|
|
15
|
+
* are valid millisecond bounds.
|
|
16
|
+
* @param key - subscription key for the region; never copied into a result or error.
|
|
17
|
+
* @param region - `global` or `cn`, which selects the fixed quota host.
|
|
18
|
+
* @param http - fetch implementation (injectable for tests).
|
|
19
|
+
* @param signal - caller cancellation from the RPC transport.
|
|
20
|
+
* @returns the finite windows in the order the API lists them.
|
|
21
|
+
* @throws when the key is pay-as-you-go, the endpoint or its business status
|
|
22
|
+
* fails, the body has no `model_remains`, or no window is a finite bar.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fetchMiniMaxUsage(key: string, region: 'global' | 'cn', http?: typeof fetch, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Used percentage from the legacy counts, read as the official CLI does when no
|
|
3
|
+
* explicit percentage exists: the count is the quota remaining. Undefined
|
|
4
|
+
* unless the total is a positive finite number, so a zero total never yields a
|
|
5
|
+
* bar.
|
|
6
|
+
*/
|
|
7
|
+
function usedFromCounts(total, count) {
|
|
8
|
+
return typeof total === 'number' && Number.isFinite(total) && total > 0 && typeof count === 'number'
|
|
9
|
+
? (1 - count / total) * 100
|
|
10
|
+
: undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Read MiniMax subscription quota (M Plan and Token Plan), not the balance API
|
|
14
|
+
* that `sk-api-*` pay-as-you-go keys use.
|
|
15
|
+
*
|
|
16
|
+
* Each `model_remains` row can yield an interval window and a weekly window.
|
|
17
|
+
* A window needs a finite share: the explicit `*_remaining_percent` (the weekly
|
|
18
|
+
* one scaled by `weekly_boost_permille`), else the legacy counts. Time-based
|
|
19
|
+
* plans report zero counts beside the percentage for standard models, so the
|
|
20
|
+
* percentage stands on its own and a zero total does not hide the window.
|
|
21
|
+
* Windows that cannot be a finite bar are omitted rather than approximated:
|
|
22
|
+
* status 3 (unlimited, or no quota in the plan), a weekly pool boosted past
|
|
23
|
+
* 100% remaining, a percentage outside 0-100, and a row with neither a
|
|
24
|
+
* percentage nor positive counts. Start and end times are kept only when both
|
|
25
|
+
* are valid millisecond bounds.
|
|
26
|
+
* @param key - subscription key for the region; never copied into a result or error.
|
|
27
|
+
* @param region - `global` or `cn`, which selects the fixed quota host.
|
|
28
|
+
* @param http - fetch implementation (injectable for tests).
|
|
29
|
+
* @param signal - caller cancellation from the RPC transport.
|
|
30
|
+
* @returns the finite windows in the order the API lists them.
|
|
31
|
+
* @throws when the key is pay-as-you-go, the endpoint or its business status
|
|
32
|
+
* fails, the body has no `model_remains`, or no window is a finite bar.
|
|
33
|
+
*/
|
|
34
|
+
export async function fetchMiniMaxUsage(key, region, http = fetch, signal) {
|
|
35
|
+
if (key.startsWith('sk-api-'))
|
|
36
|
+
throw new Error('MiniMax requires a subscription key, not a pay-as-you-go key');
|
|
37
|
+
const response = await http(`https://www.minimax.${region === 'cn' ? 'cn' : 'io'}/v1/token_plan/remains`, {
|
|
38
|
+
headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json' },
|
|
39
|
+
...(signal === undefined ? {} : { signal }),
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok)
|
|
42
|
+
throw new Error(`MiniMax usage endpoint returned HTTP ${response.status}`);
|
|
43
|
+
const body = await response.json();
|
|
44
|
+
if ((body?.base_resp?.status_code !== undefined && body.base_resp.status_code !== 0) || !Array.isArray(body?.model_remains)) {
|
|
45
|
+
throw new Error('MiniMax usage response failed or has no quota records');
|
|
46
|
+
}
|
|
47
|
+
const windows = [];
|
|
48
|
+
for (const raw of body.model_remains) {
|
|
49
|
+
if (raw === null || typeof raw !== 'object')
|
|
50
|
+
continue;
|
|
51
|
+
const row = raw;
|
|
52
|
+
if (typeof row.model_name !== 'string')
|
|
53
|
+
continue;
|
|
54
|
+
for (const [prefix, startKey, endKey, fallback] of [
|
|
55
|
+
['current_interval', 'start_time', 'end_time', 'session'],
|
|
56
|
+
['current_weekly', 'weekly_start_time', 'weekly_end_time', 'weekly'],
|
|
57
|
+
]) {
|
|
58
|
+
const status = row[`${prefix}_status`];
|
|
59
|
+
if (status === 3)
|
|
60
|
+
continue; // Unlimited pools cannot have a finite percentage.
|
|
61
|
+
const remaining = row[`${prefix}_remaining_percent`];
|
|
62
|
+
const boost = prefix === 'current_weekly' ? row.weekly_boost_permille ?? 1000 : 1000;
|
|
63
|
+
if (typeof boost !== 'number' || !Number.isFinite(boost) || boost <= 0)
|
|
64
|
+
continue;
|
|
65
|
+
// An explicit percentage needs no counts: time-based plans send zero
|
|
66
|
+
// counts beside it. Official CLI compatibility: without a percentage, the
|
|
67
|
+
// legacy counts are the quota remaining.
|
|
68
|
+
const usedPercent = typeof remaining === 'number' ? 100 - remaining * boost / 1000
|
|
69
|
+
: usedFromCounts(row[`${prefix}_total_count`], row[`${prefix}_usage_count`]);
|
|
70
|
+
if (usedPercent === undefined || !Number.isFinite(usedPercent) || usedPercent < 0 || usedPercent > 100)
|
|
71
|
+
continue;
|
|
72
|
+
const start = row[startKey];
|
|
73
|
+
const end = row[endKey];
|
|
74
|
+
const validBounds = typeof start === 'number' && Number.isFinite(start) && start > 0
|
|
75
|
+
&& typeof end === 'number' && Number.isFinite(end) && end > start;
|
|
76
|
+
windows.push({ kind: fallback === 'session' && validBounds && end - start !== 18_000_000 ? 'other' : fallback,
|
|
77
|
+
scope: row.model_name, usedPercent,
|
|
78
|
+
...(validBounds ? { startsAt: start, resetsAt: end } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!windows.length)
|
|
83
|
+
throw new Error('MiniMax has no supported finite quota windows');
|
|
84
|
+
return { supported: true, plan: 'MiniMax', windows };
|
|
85
|
+
}
|
|
@@ -107,7 +107,7 @@ export class PoolUsageTracker {
|
|
|
107
107
|
// every display surface — this is what was previously showing, so
|
|
108
108
|
// keep showing it (even through a forced refresh: retrying past the
|
|
109
109
|
// cooldown is exactly the retry storm `cooldownMs` exists to avoid).
|
|
110
|
-
return entry.lastSnapshot;
|
|
110
|
+
return { ...entry.lastSnapshot, stale: true };
|
|
111
111
|
}
|
|
112
112
|
else {
|
|
113
113
|
throw entry.error;
|
|
@@ -122,7 +122,7 @@ export class PoolUsageTracker {
|
|
|
122
122
|
// snapshot was on record before it ran onto the new failure entry.
|
|
123
123
|
const failed = this.entries.get(key);
|
|
124
124
|
if (failed?.snapshot === undefined && failed?.lastSnapshot !== undefined)
|
|
125
|
-
return failed.lastSnapshot;
|
|
125
|
+
return { ...failed.lastSnapshot, stale: true };
|
|
126
126
|
throw error;
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -155,8 +155,10 @@ export class PoolUsageTracker {
|
|
|
155
155
|
// consecutive failure, even though nothing newer ever replaced it.
|
|
156
156
|
const prior = this.entries.get(key);
|
|
157
157
|
const lastSnapshot = prior?.snapshot ?? prior?.lastSnapshot;
|
|
158
|
-
pending = fetcher().then((
|
|
159
|
-
|
|
158
|
+
pending = fetcher().then((value) => {
|
|
159
|
+
const at = Date.now();
|
|
160
|
+
const snapshot = { ...value, observedAt: at, stale: false };
|
|
161
|
+
this.entries.set(key, { snapshot, at });
|
|
160
162
|
return snapshot;
|
|
161
163
|
}, (error) => {
|
|
162
164
|
if (!isMissingOrInvalidCredential(error)) {
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subscriptions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Unified subscription providers and usage dashboard for DeepSeek Harness",
|
|
5
|
+
"icon": "./icon.svg",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
@@ -43,12 +44,15 @@
|
|
|
43
44
|
"types": "./vendor/cursor/index.d.ts",
|
|
44
45
|
"default": "./vendor/cursor/index.js"
|
|
45
46
|
},
|
|
46
|
-
"./package.json": "./package.json"
|
|
47
|
+
"./package.json": "./package.json",
|
|
48
|
+
"./locale/*.json": "./locale/*.json"
|
|
47
49
|
},
|
|
48
50
|
"files": [
|
|
49
51
|
"lib",
|
|
50
52
|
"vendor/cursor",
|
|
51
|
-
"cordis.patch.yml"
|
|
53
|
+
"cordis.patch.yml",
|
|
54
|
+
"locale/*.json",
|
|
55
|
+
"icon.svg"
|
|
52
56
|
],
|
|
53
57
|
"dsh": {
|
|
54
58
|
"bundle": {
|