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 +20 -20
- package/README.en.md +9 -9
- package/README.md +6 -6
- package/cordis.patch.yml +5 -5
- package/lib/client.js +17 -34
- package/lib/index.js +156 -35
- package/package.json +1 -1
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
|
|
73
|
-
quota
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
模型選單只顯示 Anthropic Models API 回傳的模型。**設定 → Claude 訂閱**的
|
|
70
|
+
**訂閱額度**卡片只顯示 Anthropic usage API 回傳的額度視窗與名稱。
|
|
71
|
+
資料由 host 端讀取,access token 不會離開本機。結果快取 60 秒,
|
|
72
|
+
按**重新整理**可立即重新讀取。
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
543
|
-
|
|
544
|
-
|
|
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
|
|
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
|
|
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,
|
|
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(
|
|
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 =
|
|
526
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
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.
|
|
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",
|