dsh-plugin-subscriptions 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/README.zh.md +5 -5
- package/lib/auth/claude-code-creds.d.ts +23 -0
- package/lib/auth/claude-code-creds.js +167 -0
- package/lib/auth/pkce.js +1 -1
- package/lib/client/SubscriptionsSection.js +6 -1
- package/lib/client.js +5 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +395 -52
- package/lib/providers/catalog-store.js +4 -0
- package/lib/providers/claude.d.ts +35 -3
- package/lib/providers/claude.js +181 -27
- package/lib/providers/common.d.ts +2 -0
- package/package.json +6 -8
package/lib/providers/claude.js
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* platform.claude.com with the Claude Code client id, and streaming against
|
|
4
4
|
* the Anthropic Messages API with the Claude Code identity headers.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId, resolveRetryPolicy } from '@deepseek-ai/dsh-llm';
|
|
7
8
|
import { resolveImages } from '../translate/resolved.js';
|
|
8
9
|
import { streamAnthropic, toAnthropicMessages, toAnthropicSystem, toAnthropicTools, } from '../translate/anthropic.js';
|
|
9
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
|
|
10
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
|
|
10
11
|
export const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
11
|
-
export const CLAUDE_AUTHORIZE_URL = 'https://claude.
|
|
12
|
-
export const CLAUDE_TOKEN_URL = 'https://
|
|
12
|
+
export const CLAUDE_AUTHORIZE_URL = 'https://claude.com/cai/oauth/authorize';
|
|
13
|
+
export const CLAUDE_TOKEN_URL = 'https://claude.ai/v1/oauth/token';
|
|
13
14
|
export const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages?beta=true';
|
|
14
15
|
export const CLAUDE_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
|
|
16
|
+
export const CLAUDE_MODELS_URL = 'https://api.anthropic.com/v1/models?beta=true';
|
|
15
17
|
const CLAUDE_SCOPE = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
|
|
16
18
|
const CLAUDE_CALLBACK_PATH = '/callback';
|
|
17
19
|
const CLAUDE_CONTEXT_WINDOW = 200_000;
|
|
@@ -23,19 +25,39 @@ export const CLAUDE_PREEMPT_MS = 5 * 60_000;
|
|
|
23
25
|
* so these headers impersonate the CLI; the harness attribution user-agent
|
|
24
26
|
* cannot be sent here (one user-agent slot, and the CLI's wins).
|
|
25
27
|
*/
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
+
export const CLAUDE_CLI_FALLBACK_VERSION = '2.1.234';
|
|
29
|
+
export function detectClaudeVersion() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = execFileSync('claude', ['--version'], { timeout: 3000, encoding: 'utf8' });
|
|
32
|
+
const match = raw.match(/^(\d+\.\d+\.\d+)/);
|
|
33
|
+
if (match)
|
|
34
|
+
return match[1];
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
return CLAUDE_CLI_FALLBACK_VERSION;
|
|
38
|
+
}
|
|
39
|
+
const CLAUDE_CLI_USER_AGENT = `claude-cli/${detectClaudeVersion()} (external, cli)`;
|
|
40
|
+
export const CLAUDE_BETA_FALLBACK = [
|
|
41
|
+
'claude-code-20250219',
|
|
42
|
+
'oauth-2025-04-20',
|
|
43
|
+
'interleaved-thinking-2025-05-14',
|
|
44
|
+
'context-management-2025-06-27',
|
|
45
|
+
'effort-2025-11-24',
|
|
46
|
+
'compact-2026-01-12',
|
|
47
|
+
'files-api-2025-04-14',
|
|
48
|
+
].join(',');
|
|
49
|
+
const CLAUDE_BETA_FLAGS = CLAUDE_BETA_FALLBACK;
|
|
28
50
|
/** Static claude flow facts for the OAuth flow engine. */
|
|
29
51
|
export const claudeFlow = {
|
|
30
52
|
callbackPath: CLAUDE_CALLBACK_PATH,
|
|
31
53
|
// The redirect URI embeds the port, so it must be an ephemeral one.
|
|
32
|
-
listen: { host: '
|
|
54
|
+
listen: { host: '127.0.0.1', ports: [0] },
|
|
33
55
|
buildAuthorizeUrl({ redirectUri, state, pkce }) {
|
|
34
56
|
const params = new URLSearchParams({
|
|
35
57
|
code: 'true',
|
|
36
58
|
client_id: CLAUDE_CLIENT_ID,
|
|
37
59
|
response_type: 'code',
|
|
38
|
-
redirect_uri:
|
|
60
|
+
redirect_uri: 'https://platform.claude.com/oauth/code/callback',
|
|
39
61
|
scope: CLAUDE_SCOPE,
|
|
40
62
|
code_challenge: pkce.challenge,
|
|
41
63
|
code_challenge_method: 'S256',
|
|
@@ -236,24 +258,88 @@ export async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
|
|
|
236
258
|
}
|
|
237
259
|
return { supported: true, windows };
|
|
238
260
|
}
|
|
261
|
+
function claudeThinkingType(capabilities) {
|
|
262
|
+
const types = capabilities?.thinking?.types;
|
|
263
|
+
if (types?.enabled?.supported === true)
|
|
264
|
+
return 'enabled';
|
|
265
|
+
if (types?.adaptive?.supported === true)
|
|
266
|
+
return 'adaptive';
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
/** Effort levels in display order; a model exposes only the ones it advertises as supported. */
|
|
270
|
+
const CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
271
|
+
function claudeReasoning(capabilities) {
|
|
272
|
+
const effort = capabilities?.effort;
|
|
273
|
+
if (effort?.supported !== true)
|
|
274
|
+
return undefined;
|
|
275
|
+
const efforts = CLAUDE_EFFORT_LEVELS
|
|
276
|
+
.filter(level => effort[level]?.supported === true)
|
|
277
|
+
.map(level => ({ id: ReasoningEffortId(level), name: level[0].toUpperCase() + level.slice(1) }));
|
|
278
|
+
return efforts.length > 0 ? { efforts } : undefined;
|
|
279
|
+
}
|
|
280
|
+
/** Fetch the live model catalog from the subscription endpoint. */
|
|
281
|
+
export async function fetchClaudeModels(session, fetchFn = fetch) {
|
|
282
|
+
const response = await fetchFn(CLAUDE_MODELS_URL, {
|
|
283
|
+
headers: {
|
|
284
|
+
'authorization': `Bearer ${session.accessToken}`,
|
|
285
|
+
'anthropic-version': '2023-06-01',
|
|
286
|
+
'user-agent': CLAUDE_CLI_USER_AGENT,
|
|
287
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
288
|
+
'accept': 'application/json',
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
if (!response.ok)
|
|
292
|
+
throw await httpLlmError(response, 'claude models API');
|
|
293
|
+
const payload = await response.json();
|
|
294
|
+
if (!Array.isArray(payload.data)) {
|
|
295
|
+
throw new Error('claude models API returned an invalid catalog');
|
|
296
|
+
}
|
|
297
|
+
const models = payload.data
|
|
298
|
+
.filter((m) => typeof m.id === 'string')
|
|
299
|
+
.map((m) => {
|
|
300
|
+
const thinkingType = claudeThinkingType(m.capabilities);
|
|
301
|
+
const reasoning = claudeReasoning(m.capabilities);
|
|
302
|
+
return {
|
|
303
|
+
id: m.id,
|
|
304
|
+
name: m.display_name ?? m.id,
|
|
305
|
+
...thinkingType === undefined ? {} : { thinkingType },
|
|
306
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
if (models.length === 0) {
|
|
310
|
+
throw new Error('claude models API returned an empty catalog');
|
|
311
|
+
}
|
|
312
|
+
return models;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Claude Code's own SDK retry shape: exponential backoff starting at 1s,
|
|
316
|
+
* doubling per attempt, capped at 60s, plus jitter. `maxRetries` is the
|
|
317
|
+
* count of retries after the first attempt (Claude Code defaults to 10).
|
|
318
|
+
*/
|
|
319
|
+
const CLAUDE_RETRY_INITIAL_DELAY_MS = 1_000;
|
|
320
|
+
const CLAUDE_RETRY_MAX_DELAY_MS = 60_000;
|
|
321
|
+
const CLAUDE_RETRY_JITTER_RATIO = 0.2;
|
|
239
322
|
/** The Claude 4.5 family accepts image input. */
|
|
240
323
|
const CLAUDE_MODALITIES = ['text', 'image'];
|
|
241
324
|
/** Claude wire adapter: one instance serves the `claude` provider route. */
|
|
242
325
|
export class ClaudeAdapter extends LlmAdapter {
|
|
243
326
|
options;
|
|
327
|
+
catalog;
|
|
244
328
|
constructor(options) {
|
|
245
329
|
super();
|
|
246
330
|
this.options = options;
|
|
331
|
+
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
247
332
|
}
|
|
248
|
-
|
|
249
|
-
return
|
|
333
|
+
async fetchCatalog() {
|
|
334
|
+
return fetchClaudeModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
250
335
|
}
|
|
251
|
-
async
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
336
|
+
async discovered(model) {
|
|
337
|
+
if (!this.options.discovery)
|
|
338
|
+
return undefined;
|
|
339
|
+
const models = await this.catalog.resolve(() => this.fetchCatalog());
|
|
340
|
+
return models?.find(entry => entry.id === model);
|
|
341
|
+
}
|
|
342
|
+
staticModels(provider) {
|
|
257
343
|
return this.options.models.map(model => ({
|
|
258
344
|
provider,
|
|
259
345
|
id: model.id,
|
|
@@ -261,18 +347,61 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
261
347
|
inputModalities: model.inputModalities ?? CLAUDE_MODALITIES,
|
|
262
348
|
}));
|
|
263
349
|
}
|
|
264
|
-
|
|
350
|
+
providerInfo(provider) {
|
|
351
|
+
return { id: provider, name: 'Claude (Subscription)' };
|
|
352
|
+
}
|
|
353
|
+
providerRetryPolicy(provider) {
|
|
354
|
+
if (this.options.maxRetries === undefined)
|
|
355
|
+
return undefined;
|
|
356
|
+
return resolveRetryPolicy({
|
|
357
|
+
mode: 'normal',
|
|
358
|
+
maxRetries: this.options.maxRetries,
|
|
359
|
+
backoff: {
|
|
360
|
+
initialDelayMs: CLAUDE_RETRY_INITIAL_DELAY_MS,
|
|
361
|
+
maxDelayMs: CLAUDE_RETRY_MAX_DELAY_MS,
|
|
362
|
+
jitterRatio: CLAUDE_RETRY_JITTER_RATIO,
|
|
363
|
+
},
|
|
364
|
+
}, `claude: provider "${provider}" retryPolicy`);
|
|
365
|
+
}
|
|
366
|
+
async listModels(provider) {
|
|
367
|
+
if (await this.options.tokens.peek() === undefined)
|
|
368
|
+
return [];
|
|
369
|
+
if (!this.options.discovery)
|
|
370
|
+
return this.staticModels(provider);
|
|
371
|
+
try {
|
|
372
|
+
const models = await this.catalog.get(() => this.fetchCatalog());
|
|
373
|
+
return models.map(model => ({
|
|
374
|
+
provider,
|
|
375
|
+
id: model.id,
|
|
376
|
+
name: model.name,
|
|
377
|
+
inputModalities: CLAUDE_MODALITIES,
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (error instanceof LlmError
|
|
382
|
+
&& (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL'))
|
|
383
|
+
return [];
|
|
384
|
+
if (error instanceof LlmError && error.code === 'AUTH')
|
|
385
|
+
this.catalog.invalidate();
|
|
386
|
+
this.options.onWarn?.(`claude model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
387
|
+
return this.staticModels(provider);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async resolveModel(provider, model) {
|
|
391
|
+
const disc = await this.discovered(model);
|
|
265
392
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
266
|
-
|
|
393
|
+
const reasoning = disc?.reasoning;
|
|
394
|
+
return {
|
|
267
395
|
provider,
|
|
268
396
|
id: model,
|
|
269
|
-
name: configured?.name ?? model,
|
|
397
|
+
name: disc?.name ?? configured?.name ?? model,
|
|
270
398
|
inputModalities: configured?.inputModalities ?? CLAUDE_MODALITIES,
|
|
271
|
-
context: {
|
|
399
|
+
context: {
|
|
400
|
+
contextWindow: disc?.contextWindow ?? configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW,
|
|
401
|
+
},
|
|
272
402
|
defaultMaxTokens: configured?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
});
|
|
403
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
404
|
+
};
|
|
276
405
|
}
|
|
277
406
|
async *stream(options) {
|
|
278
407
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
@@ -280,7 +409,6 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
280
409
|
let session = await this.options.tokens.session();
|
|
281
410
|
let response = await this.request(options, session, watchdog.signal);
|
|
282
411
|
if (response.status === 401) {
|
|
283
|
-
// One forced refresh + retry on an unexpired-but-rejected token.
|
|
284
412
|
session = await this.options.tokens.session(true);
|
|
285
413
|
response = await this.request(options, session, watchdog.signal);
|
|
286
414
|
}
|
|
@@ -298,18 +426,44 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
298
426
|
watchdog.stop();
|
|
299
427
|
}
|
|
300
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* `display: 'summarized'` is set explicitly on both shapes: `adaptive`-type
|
|
431
|
+
* models default to `display: 'omitted'`, which returns thinking blocks with
|
|
432
|
+
* an empty `thinking` field — without this override the "Think" panel would
|
|
433
|
+
* always render empty even though real reasoning (and billed thinking_tokens)
|
|
434
|
+
* ran.
|
|
435
|
+
*/
|
|
436
|
+
thinkingParam(thinkingType, maxTokens) {
|
|
437
|
+
if (thinkingType === 'adaptive')
|
|
438
|
+
return { type: 'adaptive', display: 'summarized' };
|
|
439
|
+
if (thinkingType === 'enabled') {
|
|
440
|
+
const budget = Math.min(Math.max(1_024, Math.floor(maxTokens * 0.5)), maxTokens - 100);
|
|
441
|
+
if (budget < 1_024)
|
|
442
|
+
return undefined;
|
|
443
|
+
return { type: 'enabled', budget_tokens: budget, display: 'summarized' };
|
|
444
|
+
}
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
301
447
|
async request(options, session, signal) {
|
|
302
448
|
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
|
|
449
|
+
const maxTokens = options.maxTokens
|
|
450
|
+
?? this.options.models.find(entry => entry.id === options.model)?.maxTokens
|
|
451
|
+
?? CLAUDE_DEFAULT_MAX_TOKENS;
|
|
452
|
+
const disc = await this.discovered(options.model);
|
|
453
|
+
const thinking = this.thinkingParam(disc?.thinkingType, maxTokens);
|
|
454
|
+
const effort = options.reasoningEffort !== undefined && disc?.reasoning !== undefined
|
|
455
|
+
? { output_config: { effort: String(options.reasoningEffort) } }
|
|
456
|
+
: {};
|
|
303
457
|
const body = {
|
|
304
458
|
model: options.model,
|
|
305
|
-
max_tokens:
|
|
306
|
-
?? this.options.models.find(entry => entry.id === options.model)?.maxTokens
|
|
307
|
-
?? CLAUDE_DEFAULT_MAX_TOKENS,
|
|
459
|
+
max_tokens: maxTokens,
|
|
308
460
|
system: toAnthropicSystem(options.system, messages),
|
|
309
461
|
messages: toAnthropicMessages(messages),
|
|
310
462
|
...options.tools !== undefined && options.tools.length > 0
|
|
311
463
|
? { tools: toAnthropicTools(options.tools) }
|
|
312
464
|
: {},
|
|
465
|
+
...thinking === undefined ? {} : { thinking },
|
|
466
|
+
...effort,
|
|
313
467
|
stream: true,
|
|
314
468
|
...options.sessionId !== undefined ? { metadata: { user_id: String(options.sessionId) } } : {},
|
|
315
469
|
};
|
|
@@ -175,6 +175,8 @@ export interface DiscoveredModel {
|
|
|
175
175
|
}[];
|
|
176
176
|
defaultEffort?: ReasoningEffortId;
|
|
177
177
|
};
|
|
178
|
+
/** Claude-specific: which extended-thinking wire shape this model accepts. */
|
|
179
|
+
thinkingType?: 'enabled' | 'adaptive';
|
|
178
180
|
}
|
|
179
181
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
180
182
|
export declare const DISCOVERY_TTL_MS: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscriptions",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Use ChatGPT (Codex), Claude, and Grok (X Premium) subscriptions as DeepSeek Harness LLM providers, with OAuth login from the web Settings page",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,12 +48,6 @@
|
|
|
48
48
|
]
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
|
-
"scripts": {
|
|
52
|
-
"build": "tsc && tsdown",
|
|
53
|
-
"test": "tsc -p tsconfig.test.json && node --test lib-test/test/",
|
|
54
|
-
"prepare": "tsdown -c tsdown.prepare.config.ts",
|
|
55
|
-
"prepublishOnly": "pnpm build && pnpm test"
|
|
56
|
-
},
|
|
57
51
|
"peerDependencies": {
|
|
58
52
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
59
53
|
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.5",
|
|
@@ -81,5 +75,9 @@
|
|
|
81
75
|
"react": "^18.2.0",
|
|
82
76
|
"tsdown": "^0.15.0",
|
|
83
77
|
"typescript": "^5.8.0"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"build": "tsc && tsdown",
|
|
81
|
+
"test": "tsc -p tsconfig.test.json && node --test lib-test/test/"
|
|
84
82
|
}
|
|
85
|
-
}
|
|
83
|
+
}
|