dsh-claude-subscription 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ export declare const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
2
+ export declare const CLAUDE_MODELS_URL = "https://api.anthropic.com/v1/models?limit=1000";
3
+ export interface ClaudeModel {
4
+ readonly id: string;
5
+ readonly name: string;
6
+ readonly api: "anthropic-messages";
7
+ readonly provider: "anthropic";
8
+ readonly baseUrl: "https://api.anthropic.com";
9
+ readonly reasoning: boolean;
10
+ readonly input: readonly ["text"] | readonly ["text", "image"];
11
+ readonly cost: {
12
+ readonly input: 0;
13
+ readonly output: 0;
14
+ readonly cacheRead: 0;
15
+ readonly cacheWrite: 0;
16
+ };
17
+ readonly contextWindow: number;
18
+ readonly maxTokens: number;
19
+ }
20
+ export interface UsageWindow {
21
+ readonly id: string;
22
+ readonly label: string;
23
+ readonly remainingPercent: number;
24
+ readonly appliesTo?: string | undefined;
25
+ readonly resetsAt?: number | undefined;
26
+ }
27
+ export interface ExtraUsage {
28
+ readonly enabled: boolean;
29
+ readonly currency?: string | undefined;
30
+ readonly decimalPlaces?: number | undefined;
31
+ readonly monthlyLimit?: number | undefined;
32
+ readonly usedCredits?: number | undefined;
33
+ readonly utilization?: number | undefined;
34
+ }
35
+ export interface UsageProjection {
36
+ readonly windows: readonly UsageWindow[];
37
+ readonly extraUsage?: ExtraUsage | undefined;
38
+ }
39
+ export interface FetchedUsage extends UsageProjection {
40
+ readonly fetchedAt: number;
41
+ }
42
+ export interface AuthResolution {
43
+ readonly auth?: {
44
+ readonly apiKey?: string | undefined;
45
+ } | undefined;
46
+ }
47
+ export interface ModelReaderOptions {
48
+ readonly getAuth: (options?: {
49
+ readonly signal?: AbortSignal | undefined;
50
+ }) => Promise<AuthResolution | undefined>;
51
+ readonly setModels: (models: readonly ClaudeModel[]) => void;
52
+ readonly fetch?: typeof fetch | undefined;
53
+ readonly now?: (() => number) | undefined;
54
+ readonly ttlMs?: number | undefined;
55
+ readonly timeoutMs?: number | undefined;
56
+ }
57
+ export interface ModelReader {
58
+ readonly read: (options?: {
59
+ readonly force?: boolean | undefined;
60
+ readonly signal?: AbortSignal | undefined;
61
+ }) => Promise<unknown>;
62
+ readonly clear: () => void;
63
+ }
64
+ export interface UsageReaderOptions {
65
+ readonly getAuth: (options?: {
66
+ readonly signal?: AbortSignal | undefined;
67
+ }) => Promise<AuthResolution | undefined>;
68
+ readonly fetch?: typeof fetch | undefined;
69
+ readonly now?: (() => number) | undefined;
70
+ readonly ttlMs?: number | undefined;
71
+ readonly timeoutMs?: number | undefined;
72
+ readonly userAgent?: string | undefined;
73
+ }
74
+ export interface UsageReader {
75
+ readonly read: (options?: {
76
+ readonly force?: boolean | undefined;
77
+ readonly signal?: AbortSignal | undefined;
78
+ }) => Promise<FetchedUsage>;
79
+ readonly clear: () => void;
80
+ }
81
+ export declare function parseClaudeModels(value: unknown): ClaudeModel[];
82
+ export declare function createClaudeModelReader(options: ModelReaderOptions): ModelReader;
83
+ /** Reduce the provider payload to a browser-safe quota projection. */
84
+ export declare function parseClaudeUsage(value: unknown): UsageProjection;
85
+ /**
86
+ * Read quota through the same refreshable OAuth lifecycle used by model turns.
87
+ * The browser receives only a parsed quota projection; the bearer token is a
88
+ * request-local host value. Concurrent polls share one request.
89
+ */
90
+ export declare function createClaudeUsageReader(options: UsageReaderOptions): UsageReader;
package/lib/usage.js ADDED
@@ -0,0 +1,303 @@
1
+ export const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
2
+ export const CLAUDE_MODELS_URL = "https://api.anthropic.com/v1/models?limit=1000";
3
+ const DEFAULT_TTL_MS = 60_000;
4
+ const DEFAULT_TIMEOUT_MS = 15_000;
5
+ const OAUTH_BETA = "oauth-2025-04-20";
6
+ function record(value) {
7
+ return value !== null && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+ export function parseClaudeModels(value) {
10
+ if (!record(value) || !Array.isArray(value.data))
11
+ throw new Error("Claude returned a malformed model list");
12
+ return value.data.map((entry) => {
13
+ if (!record(entry) ||
14
+ typeof entry.id !== "string" ||
15
+ entry.id.length === 0 ||
16
+ typeof entry.display_name !== "string" ||
17
+ entry.display_name.length === 0)
18
+ throw new Error("Claude returned a malformed model");
19
+ if (!Number.isSafeInteger(entry.max_input_tokens) ||
20
+ entry.max_input_tokens <= 0 ||
21
+ !Number.isSafeInteger(entry.max_tokens) ||
22
+ entry.max_tokens <= 0)
23
+ throw new Error("Claude returned invalid model limits");
24
+ const capabilities = record(entry.capabilities) ? entry.capabilities : {};
25
+ const thinkingSupported = record(capabilities.thinking) && capabilities.thinking.supported === true;
26
+ const imageSupported = record(capabilities.image_input) && capabilities.image_input.supported === true;
27
+ return {
28
+ id: entry.id,
29
+ name: entry.display_name,
30
+ api: "anthropic-messages",
31
+ provider: "anthropic",
32
+ baseUrl: "https://api.anthropic.com",
33
+ reasoning: thinkingSupported,
34
+ input: imageSupported ? ["text", "image"] : ["text"],
35
+ cost: {
36
+ input: 0,
37
+ output: 0,
38
+ cacheRead: 0,
39
+ cacheWrite: 0,
40
+ },
41
+ contextWindow: entry.max_input_tokens,
42
+ maxTokens: entry.max_tokens,
43
+ };
44
+ });
45
+ }
46
+ export function createClaudeModelReader(options) {
47
+ const getAuth = options.getAuth;
48
+ const setModels = options.setModels;
49
+ const fetchModels = options.fetch ?? fetch;
50
+ const now = options.now ?? Date.now;
51
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
52
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
53
+ let fetchedAt = 0;
54
+ let inFlight;
55
+ const load = async (signal) => {
56
+ const access = (await getAuth({ signal }))?.auth?.apiKey;
57
+ if (typeof access !== "string" || access.length === 0)
58
+ throw new Error("Claude subscription is not signed in");
59
+ const response = await fetchModels(CLAUDE_MODELS_URL, {
60
+ method: "GET",
61
+ redirect: "error",
62
+ headers: {
63
+ authorization: `Bearer ${access}`,
64
+ "anthropic-beta": OAUTH_BETA,
65
+ "anthropic-version": "2023-06-01",
66
+ accept: "application/json",
67
+ "cache-control": "no-store",
68
+ },
69
+ signal: requestSignal(signal, timeoutMs),
70
+ });
71
+ if (!response.ok)
72
+ throw new Error(`Claude model request failed (HTTP ${response.status})`);
73
+ const models = parseClaudeModels(await response.json());
74
+ setModels(models);
75
+ fetchedAt = now();
76
+ return models;
77
+ };
78
+ return Object.freeze({
79
+ read({ force = false, signal, } = {}) {
80
+ if (!force && fetchedAt !== 0 && now() - fetchedAt < ttlMs)
81
+ return Promise.resolve();
82
+ if (inFlight !== undefined)
83
+ return inFlight;
84
+ const current = load(signal).finally(() => {
85
+ if (inFlight === current)
86
+ inFlight = undefined;
87
+ });
88
+ inFlight = current;
89
+ return current;
90
+ },
91
+ clear() {
92
+ fetchedAt = 0;
93
+ setModels([]);
94
+ },
95
+ });
96
+ }
97
+ function epochSecondsOf(value) {
98
+ if (typeof value !== "string" || value.length === 0)
99
+ return undefined;
100
+ const parsed = Date.parse(value);
101
+ return Number.isFinite(parsed) ? Math.floor(parsed / 1_000) : undefined;
102
+ }
103
+ function windowOf(id, label, utilization, resetsAtValue, appliesTo) {
104
+ if (typeof utilization !== "number" || !Number.isFinite(utilization) || utilization < 0 || utilization > 100)
105
+ throw new Error("Claude returned an invalid utilization percentage");
106
+ const resetsAt = epochSecondsOf(resetsAtValue);
107
+ return {
108
+ id,
109
+ label,
110
+ remainingPercent: 100 - utilization,
111
+ ...(appliesTo === undefined ? {} : { appliesTo }),
112
+ ...(resetsAt === undefined ? {} : { resetsAt }),
113
+ };
114
+ }
115
+ function decimalOf(value, label) {
116
+ if (value === undefined || value === null)
117
+ return undefined;
118
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
119
+ throw new Error(`Claude returned an invalid ${label}`);
120
+ return value;
121
+ }
122
+ function extraUsageOf(value, spend) {
123
+ if (record(spend) && typeof spend.enabled === "boolean") {
124
+ const used = record(spend.used) ? spend.used : undefined;
125
+ const limit = record(spend.limit) ? spend.limit : undefined;
126
+ const usedCredits = decimalOf(used?.amount_minor, "used credit amount");
127
+ const monthlyLimit = decimalOf(limit?.amount_minor, "monthly credit limit");
128
+ const usedCurrency = used?.currency;
129
+ const limitCurrency = limit?.currency;
130
+ const currency = (usedCurrency ?? limitCurrency);
131
+ const decimalPlaces = (used?.exponent ?? limit?.exponent);
132
+ if (currency !== undefined &&
133
+ (typeof currency !== "string" || !/^[A-Za-z]{3}$/.test(currency)))
134
+ throw new Error("Claude returned an invalid extra usage currency");
135
+ if (decimalPlaces !== undefined &&
136
+ (!Number.isSafeInteger(decimalPlaces) ||
137
+ decimalPlaces < 0 ||
138
+ decimalPlaces > 20))
139
+ throw new Error("Claude returned invalid extra usage decimal places");
140
+ if ((typeof usedCurrency === "string" &&
141
+ typeof limitCurrency === "string" &&
142
+ usedCurrency.toUpperCase() !== limitCurrency.toUpperCase()) ||
143
+ (Number.isSafeInteger(used?.exponent) &&
144
+ Number.isSafeInteger(limit?.exponent) &&
145
+ used?.exponent !== limit?.exponent))
146
+ throw new Error("Claude returned inconsistent extra usage units");
147
+ return {
148
+ enabled: spend.enabled,
149
+ ...(currency === undefined
150
+ ? {}
151
+ : { currency: currency.toUpperCase() }),
152
+ ...(decimalPlaces === undefined ? {} : { decimalPlaces: decimalPlaces }),
153
+ ...(monthlyLimit === undefined ? {} : { monthlyLimit }),
154
+ ...(usedCredits === undefined ? {} : { usedCredits }),
155
+ ...(typeof spend.percent === "number" && Number.isFinite(spend.percent)
156
+ ? { utilization: spend.percent }
157
+ : {}),
158
+ };
159
+ }
160
+ if (value === undefined || value === null)
161
+ return undefined;
162
+ if (!record(value))
163
+ throw new Error("Claude returned malformed extra usage details");
164
+ if (typeof value.is_enabled !== "boolean")
165
+ return undefined;
166
+ const monthlyLimit = decimalOf(value.monthly_limit, "monthly credit limit");
167
+ const usedCredits = decimalOf(value.used_credits, "used credit amount");
168
+ const utilization = decimalOf(value.utilization, "extra usage utilization");
169
+ const currency = value.currency;
170
+ const decimalPlaces = value.decimal_places;
171
+ if (currency !== undefined &&
172
+ currency !== null &&
173
+ (typeof currency !== "string" || !/^[A-Za-z]{3}$/.test(currency)))
174
+ throw new Error("Claude returned an invalid extra usage currency");
175
+ if (decimalPlaces !== undefined &&
176
+ decimalPlaces !== null &&
177
+ (!Number.isSafeInteger(decimalPlaces) ||
178
+ decimalPlaces < 0 ||
179
+ decimalPlaces > 20))
180
+ throw new Error("Claude returned invalid extra usage decimal places");
181
+ return {
182
+ enabled: value.is_enabled,
183
+ ...(typeof currency === "string" ? { currency: currency.toUpperCase() } : {}),
184
+ ...(Number.isSafeInteger(decimalPlaces) ? { decimalPlaces: decimalPlaces } : {}),
185
+ ...(monthlyLimit === undefined ? {} : { monthlyLimit }),
186
+ ...(usedCredits === undefined ? {} : { usedCredits }),
187
+ ...(utilization === undefined ? {} : { utilization }),
188
+ };
189
+ }
190
+ /** Reduce the provider payload to a browser-safe quota projection. */
191
+ export function parseClaudeUsage(value) {
192
+ if (!record(value))
193
+ throw new Error("Claude returned a malformed usage response");
194
+ const windows = Array.isArray(value.limits)
195
+ ? value.limits.map((entry, index) => {
196
+ if (!record(entry) || typeof entry.kind !== "string" || entry.kind.length === 0)
197
+ throw new Error("Claude returned a malformed usage window");
198
+ const scope = record(entry.scope) ? entry.scope : undefined;
199
+ const model = scope !== undefined && record(scope.model) ? scope.model : undefined;
200
+ const modelName = typeof model?.display_name === "string" && model.display_name.length > 0
201
+ ? model.display_name
202
+ : undefined;
203
+ const group = typeof entry.group === "string" && entry.group.length > 0
204
+ ? entry.group
205
+ : entry.kind;
206
+ const label = modelName === undefined ? group : `${group} (${modelName})`;
207
+ const appliesTo = model === undefined
208
+ ? "all"
209
+ : typeof model.id === "string" && model.id.length > 0
210
+ ? model.id
211
+ : undefined;
212
+ return windowOf(`${entry.kind}:${index}`, label, entry.percent, entry.resets_at, appliesTo);
213
+ })
214
+ : Object.entries(value)
215
+ .filter(([, entry]) => record(entry) && Object.hasOwn(entry, "utilization") && Object.hasOwn(entry, "resets_at"))
216
+ .map(([id, entry]) => {
217
+ const rec = entry;
218
+ return windowOf(id, id, rec.utilization, rec.resets_at);
219
+ });
220
+ const extraUsage = extraUsageOf(value.extra_usage, value.spend);
221
+ return {
222
+ windows,
223
+ ...(extraUsage === undefined ? {} : { extraUsage }),
224
+ };
225
+ }
226
+ const requestSignal = (signal, timeoutMs) => {
227
+ const timeout = AbortSignal.timeout(timeoutMs);
228
+ return signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
229
+ };
230
+ function messageForStatus(status) {
231
+ if (status === 401 || status === 403)
232
+ return "Claude sign-in needs to be renewed";
233
+ if (status === 429)
234
+ return "Claude usage requests are rate limited. Try again later";
235
+ return `Claude usage request failed (HTTP ${status})`;
236
+ }
237
+ /**
238
+ * Read quota through the same refreshable OAuth lifecycle used by model turns.
239
+ * The browser receives only a parsed quota projection; the bearer token is a
240
+ * request-local host value. Concurrent polls share one request.
241
+ */
242
+ export function createClaudeUsageReader(options) {
243
+ const getAuth = options.getAuth;
244
+ const fetchUsage = options.fetch ?? fetch;
245
+ const now = options.now ?? Date.now;
246
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
247
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
248
+ const userAgent = options.userAgent ?? "dsh-claude-subscription/0.4.0";
249
+ let cached;
250
+ let inFlight;
251
+ const load = async (signal) => {
252
+ const access = (await getAuth({ signal }))?.auth?.apiKey;
253
+ if (typeof access !== "string" || access.length === 0)
254
+ throw new Error("Claude subscription is not signed in");
255
+ const response = await fetchUsage(CLAUDE_USAGE_URL, {
256
+ method: "GET",
257
+ redirect: "error",
258
+ headers: {
259
+ authorization: `Bearer ${access}`,
260
+ "anthropic-beta": OAUTH_BETA,
261
+ accept: "application/json",
262
+ "cache-control": "no-store",
263
+ "user-agent": userAgent,
264
+ },
265
+ signal: requestSignal(signal, timeoutMs),
266
+ });
267
+ if (!response.ok)
268
+ throw new Error(messageForStatus(response.status));
269
+ let value;
270
+ try {
271
+ value = await response.json();
272
+ }
273
+ catch {
274
+ throw new Error("Claude returned an unreadable usage response");
275
+ }
276
+ return {
277
+ ...parseClaudeUsage(value),
278
+ fetchedAt: now(),
279
+ };
280
+ };
281
+ return Object.freeze({
282
+ read({ force = false, signal, } = {}) {
283
+ if (!force && cached !== undefined && now() - cached.fetchedAt < ttlMs)
284
+ return Promise.resolve(structuredClone(cached));
285
+ if (inFlight !== undefined)
286
+ return inFlight.then((v) => structuredClone(v));
287
+ const current = load(signal)
288
+ .then((value) => {
289
+ cached = structuredClone(value);
290
+ return structuredClone(value);
291
+ })
292
+ .finally(() => {
293
+ if (inFlight === current)
294
+ inFlight = undefined;
295
+ });
296
+ inFlight = current;
297
+ return current;
298
+ },
299
+ clear() {
300
+ cached = undefined;
301
+ },
302
+ });
303
+ }
package/package.json CHANGED
@@ -1,18 +1,31 @@
1
1
  {
2
2
  "name": "dsh-claude-subscription",
3
- "version": "0.3.0",
4
- "packageManager": "pnpm@11.19.0",
3
+ "version": "0.4.0",
5
4
  "description": "Claude Pro/Max subscription for DeepSeek Harness with official Anthropic OAuth login and automatic token refresh",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/x5427876/dsh-claude-subscription.git"
8
+ },
9
+ "homepage": "https://github.com/x5427876/dsh-claude-subscription",
10
+ "bugs": "https://github.com/x5427876/dsh-claude-subscription/issues",
6
11
  "type": "module",
7
12
  "main": "./lib/index.js",
13
+ "types": "./lib/types/index.d.ts",
8
14
  "exports": {
9
- ".": "./lib/index.js",
10
- "./client": "./lib/client.js",
15
+ ".": {
16
+ "types": "./lib/types/index.d.ts",
17
+ "default": "./lib/index.js"
18
+ },
19
+ "./client": {
20
+ "types": "./lib/types/client.d.ts",
21
+ "default": "./lib/client.js"
22
+ },
11
23
  "./cordis.patch.yml": "./cordis.patch.yml",
12
24
  "./package.json": "./package.json"
13
25
  },
14
26
  "files": [
15
27
  "lib/*.js",
28
+ "lib/types/**/*.d.ts",
16
29
  "cordis.patch.yml",
17
30
  "README.md",
18
31
  "README.en.md",
@@ -38,11 +51,6 @@
38
51
  "platform": "web"
39
52
  }
40
53
  },
41
- "scripts": {
42
- "build": "node --check lib/index.js && node --check lib/client.js",
43
- "test": "node --test tests/*.test.mjs",
44
- "check": "pnpm run test && pnpm run build && pnpm pack --pack-destination .artifacts"
45
- },
46
54
  "keywords": [
47
55
  "dsh-plugin",
48
56
  "deepseek",
@@ -63,21 +71,49 @@
63
71
  "node": "^22.19.0 || >=24.0.0"
64
72
  },
65
73
  "peerDependencies": {
66
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.5",
67
- "@deepseek-ai/dsh-credentials": "0.1.2-alpha.5",
68
- "@deepseek-ai/dsh-llm": "0.1.2-alpha.5",
69
- "@deepseek-ai/dsh-llm-pi-ai": "0.1.2-alpha.5",
74
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.2",
75
+ "@deepseek-ai/dsh-credentials": "0.1.6-alpha.2",
76
+ "@deepseek-ai/dsh-llm": "0.1.6-alpha.2",
77
+ "@deepseek-ai/dsh-llm-pi-ai": "0.1.6-alpha.2",
70
78
  "@deepseek-ai/schemastery": "^3.18.2",
71
- "@earendil-works/pi-ai": "^0.84.2",
79
+ "@earendil-works/pi-ai": "^0.85.1",
72
80
  "react": "^18.2.0"
73
81
  },
74
82
  "peerDependenciesMeta": {
75
- "@deepseek-ai/dsh-client-ui-primitives": { "optional": true },
76
- "@deepseek-ai/dsh-credentials": { "optional": true },
77
- "@deepseek-ai/dsh-llm": { "optional": true },
78
- "@deepseek-ai/dsh-llm-pi-ai": { "optional": true },
79
- "@deepseek-ai/schemastery": { "optional": true },
80
- "@earendil-works/pi-ai": { "optional": true },
81
- "react": { "optional": true }
83
+ "@deepseek-ai/dsh-client-ui-primitives": {
84
+ "optional": true
85
+ },
86
+ "@deepseek-ai/dsh-credentials": {
87
+ "optional": true
88
+ },
89
+ "@deepseek-ai/dsh-llm": {
90
+ "optional": true
91
+ },
92
+ "@deepseek-ai/dsh-llm-pi-ai": {
93
+ "optional": true
94
+ },
95
+ "@deepseek-ai/schemastery": {
96
+ "optional": true
97
+ },
98
+ "@earendil-works/pi-ai": {
99
+ "optional": true
100
+ },
101
+ "react": {
102
+ "optional": true
103
+ }
104
+ },
105
+ "devDependencies": {
106
+ "@types/node": "^24.13.6",
107
+ "@types/react": "~18.3.31",
108
+ "publint": "^0.3.24",
109
+ "tsx": "^4.23.15",
110
+ "typescript": "^5.9.3"
111
+ },
112
+ "scripts": {
113
+ "typecheck": "tsc --noEmit",
114
+ "build": "tsc && node scripts/build.mjs",
115
+ "pretest": "pnpm run build",
116
+ "test": "tsx --test tests/*.test.mjs",
117
+ "check": "pnpm run typecheck && pnpm run test && pnpm pack --pack-destination .artifacts"
82
118
  }
83
119
  }