dsh-claude-subscription 0.3.0 → 0.3.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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 dsh-claude-subscription contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-claude-subscription contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
package/README.en.md CHANGED
@@ -69,15 +69,15 @@ For DSH Desktop, put the `pnpm pack` tarball into
69
69
 
70
70
  ## Quota display
71
71
 
72
- The **Subscription quota** card in **Settings -> Claude subscription** shows the
73
- quota windows reported by Anthropic. The host reads them; the browser receives
74
- only a percentage projection, and the access token never leaves the machine.
75
- Results are cached for 60 seconds, and **Refresh** reads them again immediately.
76
-
77
- With **Composer quota** on, a Claude model shows the tightest applicable window
78
- next to the message input (Opus and Sonnet models also count their 7-day
79
- window). Nothing is rendered when you are signed out or when the account
80
- reports no quota windows.
72
+ The model picker shows only models returned by the Anthropic Models API. The
73
+ **Subscription quota** card in **Settings -> Claude subscription** shows only
74
+ windows and labels returned by the Anthropic usage API. The host reads them,
75
+ and the access token never leaves the machine. Results are cached for 60
76
+ seconds, and **Refresh** reads them again immediately.
77
+
78
+ With **Composer quota** on, the input shows the tightest API window explicitly
79
+ marked as global or applicable to the current model. A window without API scope
80
+ metadata is not used for composer quota.
81
81
 
82
82
  ## Update and uninstall
83
83
 
package/README.md CHANGED
@@ -66,13 +66,13 @@ DSH Desktop(桌面版)時,把 `pnpm pack` 產出的 tarball 放進
66
66
 
67
67
  ## 額度顯示
68
68
 
69
- **設定 → Claude 訂閱**的**訂閱額度**卡片顯示 Anthropic 回報的額度視窗。
70
- 資料由 host 端讀取,瀏覽器只收到百分比投影,access token 不會離開本機。
71
- 額度結果快取 60 秒,按**重新整理**可立即重新讀取。
69
+ 模型選單只顯示 Anthropic Models API 回傳的模型。**設定 → Claude 訂閱**的
70
+ **訂閱額度**卡片只顯示 Anthropic usage API 回傳的額度視窗與名稱。
71
+ 資料由 host 端讀取,access token 不會離開本機。結果快取 60 秒,
72
+ 按**重新整理**可立即重新讀取。
72
73
 
73
- 打開**輸入框額度**後,選用 Claude 模型時,輸入框右側顯示最緊的那個額度視窗
74
- (Opus 或 Sonnet 模型會納入對應的 7 天視窗)。未登入、或帳號走 API Key 計費
75
- 而沒有額度視窗時,該處不顯示任何內容。
74
+ 打開**輸入框額度**後,輸入框右側顯示 API 明確標示為全域或適用於目前模型的
75
+ 最緊額度視窗。API 未提供適用範圍時,該視窗不會用於輸入框額度。
76
76
 
77
77
  ## 更新與解除安裝
78
78
 
package/cordis.patch.yml CHANGED
@@ -1,6 +1,6 @@
1
- # One plugin owns the Claude subscription provider route, host-only account
2
- # RPC, and settings surface. It leaves DSH's model and persistent web
3
- # configuration untouched.
4
- - insert:
5
- - id: claude-subscription
1
+ # One plugin owns the Claude subscription provider route, host-only account
2
+ # RPC, and settings surface. It leaves DSH's model and persistent web
3
+ # configuration untouched.
4
+ - insert:
5
+ - id: claude-subscription
6
6
  name: 'dsh-claude-subscription'
package/lib/client.js CHANGED
@@ -8,22 +8,10 @@ window.__ModuleLoader__.load({
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
10
  //#region src/composer-quota.js
11
- const SHARED_WINDOW_IDS = Object.freeze(["five_hour", "seven_day"]);
12
- const MODEL_WINDOW_IDS = Object.freeze({
13
- opus: "seven_day_opus",
14
- sonnet: "seven_day_sonnet"
15
- });
16
11
  const isDisplayableWindow = (window) => Number.isFinite(window?.remainingPercent) && window.remainingPercent >= 0 && window.remainingPercent <= 100;
17
- const normalized = (value) => String(value ?? "").toLocaleLowerCase("en-US");
18
- function applicableIds(model) {
19
- const name = normalized(model);
20
- const family = Object.entries(MODEL_WINDOW_IDS).find(([key]) => name.includes(key));
21
- return family === void 0 ? SHARED_WINDOW_IDS : [...SHARED_WINDOW_IDS, family[1]];
22
- }
23
- /** Pick the tightest quota window that constrains the given model. */
12
+ /** Pick the tightest API-reported quota window that applies to the model. */
24
13
  function selectModelQuota(usage, model) {
25
- const ids = applicableIds(model);
26
- const windows = Array.isArray(usage?.windows) ? usage.windows.filter((window) => ids.includes(window?.id) && isDisplayableWindow(window)) : [];
14
+ const windows = Array.isArray(usage?.windows) ? usage.windows.filter((window) => (window.appliesTo === "all" || window.appliesTo === model) && isDisplayableWindow(window)) : [];
27
15
  if (windows.length === 0) return void 0;
28
16
  const selected = windows.reduce((lowest, candidate) => candidate.remainingPercent < lowest.remainingPercent ? candidate : lowest);
29
17
  return {
@@ -79,10 +67,6 @@ window.__ModuleLoader__.load({
79
67
  usageEmpty: "此帳戶沒有回傳可顯示的額度視窗。請稍後重新整理;這不代表額度為零。",
80
68
  usageUpdated: "更新於 {value}",
81
69
  remaining: "剩餘 {value}%",
82
- windowFiveHour: "5 小時額度",
83
- windowSevenDay: "7 天額度",
84
- windowSevenDayOpus: "Opus 7 天額度",
85
- windowSevenDaySonnet: "Sonnet 7 天額度",
86
70
  resets: "重置於 {value}",
87
71
  resetUnknown: "重置時間未提供",
88
72
  extraUsage: "額外用量",
@@ -118,10 +102,6 @@ window.__ModuleLoader__.load({
118
102
  usageEmpty: "This account returned no displayable quota windows. Refresh later; this does not mean zero quota.",
119
103
  usageUpdated: "Updated {value}",
120
104
  remaining: "{value}% remaining",
121
- windowFiveHour: "5-hour quota",
122
- windowSevenDay: "7-day quota",
123
- windowSevenDayOpus: "Opus 7-day quota",
124
- windowSevenDaySonnet: "Sonnet 7-day quota",
125
105
  resets: "Resets {value}",
126
106
  resetUnknown: "Reset time not provided",
127
107
  extraUsage: "Extra usage",
@@ -172,13 +152,14 @@ window.__ModuleLoader__.load({
172
152
  };
173
153
  const fill = (text, values) => Object.entries(values).reduce((next, [key, value]) => next.replace(`{${key}}`, String(value)), text);
174
154
  const percent = (value) => Number(value).toLocaleString(void 0, { maximumFractionDigits: 1 });
175
- const WINDOW_LABELS = {
176
- five_hour: "windowFiveHour",
177
- seven_day: "windowSevenDay",
178
- seven_day_opus: "windowSevenDayOpus",
179
- seven_day_sonnet: "windowSevenDaySonnet"
180
- };
181
- const windowLabel = (id, t) => t(WINDOW_LABELS[id] ?? "windowSevenDay");
155
+ const money = (value, currency, decimalPlaces) => new Intl.NumberFormat(void 0, {
156
+ style: "currency",
157
+ currency,
158
+ currencyDisplay: "code",
159
+ minimumFractionDigits: decimalPlaces,
160
+ maximumFractionDigits: decimalPlaces
161
+ }).format(value / 10 ** decimalPlaces);
162
+ const windowLabel = (window) => window.label ?? window.id;
182
163
  const validDate = (value) => {
183
164
  const date = new Date(value);
184
165
  return Number.isFinite(date.getTime()) ? date : void 0;
@@ -539,9 +520,11 @@ window.__ModuleLoader__.load({
539
520
  });
540
521
  }
541
522
  function ExtraUsageRow({ extraUsage, t }) {
542
- const detail = !extraUsage.enabled ? t("extraUsageOff") : extraUsage.usedCredits === void 0 ? t("extraUsageUnknown") : extraUsage.monthlyLimit === void 0 ? fill(t("extraUsageUsedOnly"), { used: extraUsage.usedCredits }) : fill(t("extraUsageUsed"), {
543
- used: extraUsage.usedCredits,
544
- limit: extraUsage.monthlyLimit
523
+ const amount = (value) => money(value, extraUsage.currency, extraUsage.decimalPlaces);
524
+ const hasExactAmount = extraUsage.usedCredits !== void 0 && extraUsage.currency !== void 0 && extraUsage.decimalPlaces !== void 0;
525
+ const detail = !extraUsage.enabled ? t("extraUsageOff") : !hasExactAmount ? t("extraUsageUnknown") : extraUsage.monthlyLimit === void 0 ? fill(t("extraUsageUsedOnly"), { used: amount(extraUsage.usedCredits) }) : fill(t("extraUsageUsed"), {
526
+ used: amount(extraUsage.usedCredits),
527
+ limit: amount(extraUsage.monthlyLimit)
545
528
  });
546
529
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
547
530
  className: "claudeSubscriptionExtra",
@@ -640,13 +623,13 @@ window.__ModuleLoader__.load({
640
623
  className: "claudeSubscriptionLimitTop",
641
624
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
642
625
  className: "claudeSubscriptionLimitLabel",
643
- children: windowLabel(window.id, t)
626
+ children: windowLabel(window)
644
627
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [percent(window.remainingPercent), "%"] })]
645
628
  }),
646
629
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("progress", {
647
630
  max: "100",
648
631
  value: window.remainingPercent,
649
- "aria-label": `${windowLabel(window.id, t)} ${fill(t("remaining"), { value: percent(window.remainingPercent) })}`
632
+ "aria-label": `${windowLabel(window)} ${fill(t("remaining"), { value: percent(window.remainingPercent) })}`
650
633
  }),
651
634
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
652
635
  className: "claudeSubscriptionLimitMeta",
package/lib/index.js CHANGED
@@ -431,6 +431,7 @@ function createClaudeRpcHandler(coordinator, options = {}) {
431
431
  */
432
432
  function anthropicSubscriptionProvider() {
433
433
  const provider = createAnthropicProvider();
434
+ let models = Object.freeze([]);
434
435
  const requestToken = Object.freeze({
435
436
  name: "DSH-managed Anthropic OAuth request token",
436
437
  async resolve({ credential }) {
@@ -447,7 +448,11 @@ function anthropicSubscriptionProvider() {
447
448
  auth: Object.freeze({
448
449
  ...provider.auth,
449
450
  apiKey: requestToken
450
- })
451
+ }),
452
+ getModels: () => models,
453
+ setModels(value) {
454
+ models = Object.freeze([...value]);
455
+ }
451
456
  });
452
457
  }
453
458
  const PI_AI_RUNTIME_VERSION = "0.84.2";
@@ -473,30 +478,95 @@ Object.freeze({ [QUICK_QUOTA_FIELD]: false });
473
478
  //#endregion
474
479
  //#region src/usage.js
475
480
  const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
481
+ const CLAUDE_MODELS_URL = "https://api.anthropic.com/v1/models?limit=1000";
476
482
  const DEFAULT_TTL_MS = 6e4;
477
483
  const DEFAULT_TIMEOUT_MS = 15e3;
478
484
  const OAUTH_BETA = "oauth-2025-04-20";
479
- const WINDOW_IDS = Object.freeze([
480
- "five_hour",
481
- "seven_day",
482
- "seven_day_opus",
483
- "seven_day_sonnet"
484
- ]);
485
485
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
486
+ function parseClaudeModels(value) {
487
+ if (!record(value) || !Array.isArray(value.data)) throw new Error("Claude returned a malformed model list");
488
+ return value.data.map((entry) => {
489
+ if (!record(entry) || typeof entry.id !== "string" || entry.id.length === 0 || typeof entry.display_name !== "string" || entry.display_name.length === 0) throw new Error("Claude returned a malformed model");
490
+ if (!Number.isSafeInteger(entry.max_input_tokens) || entry.max_input_tokens <= 0 || !Number.isSafeInteger(entry.max_tokens) || entry.max_tokens <= 0) throw new Error("Claude returned invalid model limits");
491
+ const capabilities = record(entry.capabilities) ? entry.capabilities : {};
492
+ return {
493
+ id: entry.id,
494
+ name: entry.display_name,
495
+ api: "anthropic-messages",
496
+ provider: "anthropic",
497
+ baseUrl: "https://api.anthropic.com",
498
+ reasoning: capabilities.thinking?.supported === true,
499
+ input: capabilities.image_input?.supported === true ? ["text", "image"] : ["text"],
500
+ cost: {
501
+ input: 0,
502
+ output: 0,
503
+ cacheRead: 0,
504
+ cacheWrite: 0
505
+ },
506
+ contextWindow: entry.max_input_tokens,
507
+ maxTokens: entry.max_tokens
508
+ };
509
+ });
510
+ }
511
+ function createClaudeModelReader(options) {
512
+ const getAuth = options.getAuth;
513
+ const setModels = options.setModels;
514
+ const fetchModels = options.fetch ?? fetch;
515
+ const now = options.now ?? Date.now;
516
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
517
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
518
+ let fetchedAt = 0;
519
+ let inFlight;
520
+ const load = async (signal) => {
521
+ const access = (await getAuth({ signal }))?.auth?.apiKey;
522
+ if (typeof access !== "string" || access.length === 0) throw new Error("Claude subscription is not signed in");
523
+ const response = await fetchModels(CLAUDE_MODELS_URL, {
524
+ method: "GET",
525
+ redirect: "error",
526
+ headers: {
527
+ authorization: `Bearer ${access}`,
528
+ "anthropic-beta": OAUTH_BETA,
529
+ "anthropic-version": "2023-06-01",
530
+ accept: "application/json",
531
+ "cache-control": "no-store"
532
+ },
533
+ signal: requestSignal(signal, timeoutMs)
534
+ });
535
+ if (!response.ok) throw new Error(`Claude model request failed (HTTP ${response.status})`);
536
+ const models = parseClaudeModels(await response.json());
537
+ setModels(models);
538
+ fetchedAt = now();
539
+ return models;
540
+ };
541
+ return Object.freeze({
542
+ read({ force = false, signal } = {}) {
543
+ if (!force && fetchedAt !== 0 && now() - fetchedAt < ttlMs) return Promise.resolve();
544
+ if (inFlight !== void 0) return inFlight;
545
+ const current = load(signal).finally(() => {
546
+ if (inFlight === current) inFlight = void 0;
547
+ });
548
+ inFlight = current;
549
+ return current;
550
+ },
551
+ clear() {
552
+ fetchedAt = 0;
553
+ setModels([]);
554
+ }
555
+ });
556
+ }
486
557
  function epochSecondsOf(value) {
487
558
  if (typeof value !== "string" || value.length === 0) return void 0;
488
559
  const parsed = Date.parse(value);
489
560
  return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : void 0;
490
561
  }
491
- function windowOf(id, value) {
492
- if (value === void 0 || value === null) return void 0;
493
- if (!record(value)) throw new Error("Claude returned a malformed usage window");
494
- const utilization = value.utilization;
562
+ function windowOf(id, label, utilization, resetsAtValue, appliesTo) {
495
563
  if (!Number.isFinite(utilization) || utilization < 0 || utilization > 100) throw new Error("Claude returned an invalid utilization percentage");
496
- const resetsAt = epochSecondsOf(value.resets_at);
564
+ const resetsAt = epochSecondsOf(resetsAtValue);
497
565
  return {
498
566
  id,
567
+ label,
499
568
  remainingPercent: 100 - utilization,
569
+ ...appliesTo === void 0 ? {} : { appliesTo },
500
570
  ...resetsAt === void 0 ? {} : { resetsAt }
501
571
  };
502
572
  }
@@ -505,15 +575,40 @@ function decimalOf(value, label) {
505
575
  if (!Number.isFinite(value) || value < 0) throw new Error(`Claude returned an invalid ${label}`);
506
576
  return value;
507
577
  }
508
- function extraUsageOf(value) {
578
+ function extraUsageOf(value, spend) {
579
+ if (record(spend) && typeof spend.enabled === "boolean") {
580
+ const used = record(spend.used) ? spend.used : void 0;
581
+ const limit = record(spend.limit) ? spend.limit : void 0;
582
+ const usedCredits = decimalOf(used?.amount_minor, "used credit amount");
583
+ const monthlyLimit = decimalOf(limit?.amount_minor, "monthly credit limit");
584
+ const currency = used?.currency ?? limit?.currency;
585
+ const decimalPlaces = used?.exponent ?? limit?.exponent;
586
+ if (currency !== void 0 && (typeof currency !== "string" || !/^[A-Za-z]{3}$/.test(currency))) throw new Error("Claude returned an invalid extra usage currency");
587
+ if (decimalPlaces !== void 0 && (!Number.isSafeInteger(decimalPlaces) || decimalPlaces < 0 || decimalPlaces > 20)) throw new Error("Claude returned invalid extra usage decimal places");
588
+ if (typeof used?.currency === "string" && typeof limit?.currency === "string" && used.currency.toUpperCase() !== limit.currency.toUpperCase() || Number.isSafeInteger(used?.exponent) && Number.isSafeInteger(limit?.exponent) && used.exponent !== limit.exponent) throw new Error("Claude returned inconsistent extra usage units");
589
+ return {
590
+ enabled: spend.enabled,
591
+ ...currency === void 0 ? {} : { currency: currency.toUpperCase() },
592
+ ...decimalPlaces === void 0 ? {} : { decimalPlaces },
593
+ ...monthlyLimit === void 0 ? {} : { monthlyLimit },
594
+ ...usedCredits === void 0 ? {} : { usedCredits },
595
+ ...Number.isFinite(spend.percent) ? { utilization: spend.percent } : {}
596
+ };
597
+ }
509
598
  if (value === void 0 || value === null) return void 0;
510
599
  if (!record(value)) throw new Error("Claude returned malformed extra usage details");
511
600
  if (typeof value.is_enabled !== "boolean") return void 0;
512
601
  const monthlyLimit = decimalOf(value.monthly_limit, "monthly credit limit");
513
602
  const usedCredits = decimalOf(value.used_credits, "used credit amount");
514
603
  const utilization = decimalOf(value.utilization, "extra usage utilization");
604
+ const currency = value.currency;
605
+ const decimalPlaces = value.decimal_places;
606
+ if (currency !== void 0 && currency !== null && (typeof currency !== "string" || !/^[A-Za-z]{3}$/.test(currency))) throw new Error("Claude returned an invalid extra usage currency");
607
+ if (decimalPlaces !== void 0 && decimalPlaces !== null && (!Number.isSafeInteger(decimalPlaces) || decimalPlaces < 0 || decimalPlaces > 20)) throw new Error("Claude returned invalid extra usage decimal places");
515
608
  return {
516
609
  enabled: value.is_enabled,
610
+ ...typeof currency === "string" ? { currency: currency.toUpperCase() } : {},
611
+ ...Number.isSafeInteger(decimalPlaces) ? { decimalPlaces } : {},
517
612
  ...monthlyLimit === void 0 ? {} : { monthlyLimit },
518
613
  ...usedCredits === void 0 ? {} : { usedCredits },
519
614
  ...utilization === void 0 ? {} : { utilization }
@@ -522,8 +617,16 @@ function extraUsageOf(value) {
522
617
  /** Reduce the provider payload to a browser-safe quota projection. */
523
618
  function parseClaudeUsage(value) {
524
619
  if (!record(value)) throw new Error("Claude returned a malformed usage response");
525
- const windows = WINDOW_IDS.map((id) => windowOf(id, value[id])).filter((entry) => entry !== void 0);
526
- const extraUsage = extraUsageOf(value.extra_usage);
620
+ const windows = Array.isArray(value.limits) ? value.limits.map((entry, index) => {
621
+ if (!record(entry) || typeof entry.kind !== "string" || entry.kind.length === 0) throw new Error("Claude returned a malformed usage window");
622
+ const model = record(entry.scope) && record(entry.scope.model) ? entry.scope.model : void 0;
623
+ const modelName = typeof model?.display_name === "string" && model.display_name.length > 0 ? model.display_name : void 0;
624
+ const group = typeof entry.group === "string" && entry.group.length > 0 ? entry.group : entry.kind;
625
+ const label = modelName === void 0 ? group : `${group} (${modelName})`;
626
+ const appliesTo = model === void 0 ? "all" : typeof model.id === "string" && model.id.length > 0 ? model.id : void 0;
627
+ return windowOf(`${entry.kind}:${index}`, label, entry.percent, entry.resets_at, appliesTo);
628
+ }) : Object.entries(value).filter(([, entry]) => record(entry) && Object.hasOwn(entry, "utilization") && Object.hasOwn(entry, "resets_at")).map(([id, entry]) => windowOf(id, id, entry.utilization, entry.resets_at));
629
+ const extraUsage = extraUsageOf(value.extra_usage, value.spend);
527
630
  return {
528
631
  windows,
529
632
  ...extraUsage === void 0 ? {} : { extraUsage }
@@ -549,7 +652,7 @@ function createClaudeUsageReader(options) {
549
652
  const now = options.now ?? Date.now;
550
653
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
551
654
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
552
- const userAgent = options.userAgent ?? "dsh-claude-subscription/0.3.0";
655
+ const userAgent = options.userAgent ?? "dsh-claude-subscription/0.3.1";
553
656
  let cached;
554
657
  let inFlight;
555
658
  const load = async (signal) => {
@@ -599,22 +702,10 @@ function createClaudeUsageReader(options) {
599
702
  }
600
703
  //#endregion
601
704
  //#region src/composer-quota.js
602
- const SHARED_WINDOW_IDS = Object.freeze(["five_hour", "seven_day"]);
603
- const MODEL_WINDOW_IDS = Object.freeze({
604
- opus: "seven_day_opus",
605
- sonnet: "seven_day_sonnet"
606
- });
607
705
  const isDisplayableWindow = (window) => Number.isFinite(window?.remainingPercent) && window.remainingPercent >= 0 && window.remainingPercent <= 100;
608
- const normalized = (value) => String(value ?? "").toLocaleLowerCase("en-US");
609
- function applicableIds(model) {
610
- const name = normalized(model);
611
- const family = Object.entries(MODEL_WINDOW_IDS).find(([key]) => name.includes(key));
612
- return family === void 0 ? SHARED_WINDOW_IDS : [...SHARED_WINDOW_IDS, family[1]];
613
- }
614
- /** Pick the tightest quota window that constrains the given model. */
706
+ /** Pick the tightest API-reported quota window that applies to the model. */
615
707
  function selectModelQuota(usage, model) {
616
- const ids = applicableIds(model);
617
- const windows = Array.isArray(usage?.windows) ? usage.windows.filter((window) => ids.includes(window?.id) && isDisplayableWindow(window)) : [];
708
+ const windows = Array.isArray(usage?.windows) ? usage.windows.filter((window) => (window.appliesTo === "all" || window.appliesTo === model) && isDisplayableWindow(window)) : [];
618
709
  if (windows.length === 0) return void 0;
619
710
  const selected = windows.reduce((lowest, candidate) => candidate.remainingPercent < lowest.remainingPercent ? candidate : lowest);
620
711
  return {
@@ -624,6 +715,28 @@ function selectModelQuota(usage, model) {
624
715
  };
625
716
  }
626
717
  //#endregion
718
+ class ClaudeApiAdapter extends PiAiAdapter {
719
+ constructor(options, modelReader) {
720
+ super(options);
721
+ this.modelReader = modelReader;
722
+ }
723
+ async listModels(provider) {
724
+ await this.modelReader.read();
725
+ return super.listModels(provider);
726
+ }
727
+ async resolveModel(provider, model, signal) {
728
+ await this.modelReader.read({ signal });
729
+ return super.resolveModel(provider, model, signal);
730
+ }
731
+ async prepareCall(provider, model, signal) {
732
+ await this.modelReader.read({ signal });
733
+ return super.prepareCall(provider, model, signal);
734
+ }
735
+ async *stream(options) {
736
+ await this.modelReader.read({ signal: options.signal });
737
+ yield* super.stream(options);
738
+ }
739
+ }
627
740
  //#region src/index.js
628
741
  const name = "claude-subscription";
629
742
  const inject = [
@@ -652,7 +765,7 @@ const KNOWN_USAGE_ERRORS = /* @__PURE__ */ new Set([
652
765
  * Extend the account RPC with the preference toggle and the quota projection.
653
766
  * Unknown endpoints stay owned by the login coordinator.
654
767
  */
655
- function createSubscriptionRpcHandler({ authHandler, usageReader, preferences }) {
768
+ function createSubscriptionRpcHandler({ authHandler, usageReader, modelReader, preferences }) {
656
769
  return async (endpoint, payload, signal) => {
657
770
  if (endpoint === "preferences/status" || endpoint === "preferences/update") try {
658
771
  signal.throwIfAborted();
@@ -684,7 +797,10 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, preferences })
684
797
  return publicError("internal", message);
685
798
  }
686
799
  const result = await authHandler(endpoint, payload, signal);
687
- if (endpoint === "logout" && result.ok === true) usageReader.clear();
800
+ if (endpoint === "logout" && result.ok === true) {
801
+ usageReader.clear();
802
+ modelReader?.clear();
803
+ }
688
804
  return result;
689
805
  };
690
806
  }
@@ -727,7 +843,11 @@ function apply(ctx) {
727
843
  });
728
844
  const profiles = /* @__PURE__ */ new Map([[PROVIDER, profile]]);
729
845
  const resolveAuth = () => authModels.getAuth(PROVIDER);
730
- const adapter = new PiAiAdapter({
846
+ const modelReader = createClaudeModelReader({
847
+ getAuth: resolveAuth,
848
+ setModels: (models) => provider.setModels(models)
849
+ });
850
+ const adapter = new ClaudeApiAdapter({
731
851
  profiles: () => profiles,
732
852
  auth,
733
853
  resolveApiKey: async () => {
@@ -741,16 +861,17 @@ function apply(ctx) {
741
861
  return resolved.auth.apiKey;
742
862
  },
743
863
  resolveAttachments: () => void 0
744
- });
864
+ }, modelReader);
745
865
  ctx.llm.registerAdapter([PROVIDER], adapter);
746
866
  const coordinator = new ClaudeLoginCoordinator(createClaudeAuthService(authModels, store));
747
867
  const usageReader = createClaudeUsageReader({ getAuth: resolveAuth });
748
868
  const handler = createSubscriptionRpcHandler({
749
869
  authHandler: createClaudeRpcHandler(coordinator, { openExternal: openClaudeAuthUrl }),
750
870
  usageReader,
871
+ modelReader,
751
872
  preferences
752
873
  });
753
874
  ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "claude-subscription: loopback account RPC");
754
875
  }
755
876
  //#endregion
756
- export { CLAUDE_USAGE_URL, ClaudeLoginCoordinator, DshOAuthCredentialStore, PI_AI_RUNTIME_VERSION, anthropicSubscriptionProvider, apply, assertClaudeAuthUrl, createClaudeAuthService, createClaudeRpcHandler, createClaudeUsageReader, createSubscriptionRpcHandler, inject, name, openClaudeAuthUrl, parseClaudeUsage, selectModelQuota };
877
+ export { CLAUDE_MODELS_URL, CLAUDE_USAGE_URL, ClaudeLoginCoordinator, DshOAuthCredentialStore, PI_AI_RUNTIME_VERSION, anthropicSubscriptionProvider, apply, assertClaudeAuthUrl, createClaudeAuthService, createClaudeModelReader, createClaudeRpcHandler, createClaudeUsageReader, createSubscriptionRpcHandler, inject, name, openClaudeAuthUrl, parseClaudeModels, parseClaudeUsage, selectModelQuota };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-claude-subscription",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "packageManager": "pnpm@11.19.0",
5
5
  "description": "Claude Pro/Max subscription for DeepSeek Harness with official Anthropic OAuth login and automatic token refresh",
6
6
  "type": "module",