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.
Files changed (41) hide show
  1. package/README.md +115 -4
  2. package/icon.svg +28 -0
  3. package/lib/client/CursorCard.js +15 -11
  4. package/lib/client/ExternalUsageCards.d.ts +11 -1
  5. package/lib/client/ExternalUsageCards.js +28 -13
  6. package/lib/client/SubscriptionUsageBadge.d.ts +25 -4
  7. package/lib/client/SubscriptionUsageBadge.js +115 -28
  8. package/lib/client/SubscriptionsSection.d.ts +7 -2
  9. package/lib/client/SubscriptionsSection.js +9 -13
  10. package/lib/client/UsageBadgeDisplaySetting.js +19 -2
  11. package/lib/client/UsageMeter.d.ts +8 -0
  12. package/lib/client/UsageMeter.js +56 -0
  13. package/lib/client/host-contract.d.ts +1 -1
  14. package/lib/client/host-contract.js +1 -0
  15. package/lib/client/locales.d.ts +50 -0
  16. package/lib/client/locales.js +52 -2
  17. package/lib/client/subscription-card-styles.js +0 -5
  18. package/lib/client/usage-badge-preferences.d.ts +7 -2
  19. package/lib/client/usage-badge-preferences.js +3 -2
  20. package/lib/client/usage-color-preferences.d.ts +6 -0
  21. package/lib/client/usage-color-preferences.js +38 -0
  22. package/lib/client/usage-pace.d.ts +42 -0
  23. package/lib/client/usage-pace.js +71 -0
  24. package/lib/client.js +489 -132
  25. package/lib/client.js.map +1 -1
  26. package/lib/index.js +252 -35
  27. package/lib/providers/claude.js +7 -0
  28. package/lib/providers/codex.js +5 -0
  29. package/lib/providers/common.d.ts +10 -0
  30. package/lib/providers/cursor-usage.js +6 -2
  31. package/lib/providers/external-usage-controller.d.ts +1 -1
  32. package/lib/providers/external-usage-controller.js +8 -3
  33. package/lib/providers/external-usage.d.ts +1 -1
  34. package/lib/providers/external-usage.js +62 -2
  35. package/lib/providers/grok.js +3 -1
  36. package/lib/providers/minimax-usage.d.ts +24 -0
  37. package/lib/providers/minimax-usage.js +85 -0
  38. package/lib/providers/pool-usage.js +6 -4
  39. package/locale/en.json +6 -0
  40. package/locale/zh.json +6 -0
  41. 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 the Kimi Code plan windows using the same API as the Kimi Code CLI. */
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 usages object');
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
  }
@@ -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
- windows.push({ kind, usedPercent, ...resetsAt === undefined ? {} : { resetsAt } });
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((snapshot) => {
159
- this.entries.set(key, { snapshot, at: Date.now() });
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
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "DSH Subscriptions",
4
+ "description": "Unified subscription providers and usage dashboard for DeepSeek Harness"
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "订阅中心",
4
+ "description": "将 ChatGPT、Claude、Grok 等订阅制模型接入 DeepSeek Harness,附带用量看板"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "dsh-subscriptions",
3
- "version": "0.1.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": {