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/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { CONTEXT_WINDOW_EXCEEDED_CODE, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, createUserMessage, errorChain, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, createUserMessage, errorChain, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { basename, dirname, join } from "node:path";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
7
10
|
import { AttachmentId } from "@deepseek-ai/dsh-attachment";
|
|
8
11
|
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
9
12
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
@@ -33,7 +36,7 @@ function createPkce() {
|
|
|
33
36
|
* @param bytes - entropy length.
|
|
34
37
|
* @returns base64url-encoded random bytes.
|
|
35
38
|
*/
|
|
36
|
-
function randomToken(bytes =
|
|
39
|
+
function randomToken(bytes = 32) {
|
|
37
40
|
return base64url(randomBytes(bytes));
|
|
38
41
|
}
|
|
39
42
|
/**
|
|
@@ -247,6 +250,187 @@ var OAuthFlowManager = class {
|
|
|
247
250
|
}
|
|
248
251
|
};
|
|
249
252
|
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/auth/claude-code-creds.ts
|
|
255
|
+
const PRIMARY_SERVICE = "Claude Code-credentials";
|
|
256
|
+
const DEFAULT_SCOPES = "user:profile user:inference user:sessions:claude_code user:mcp_servers";
|
|
257
|
+
function toSession(data) {
|
|
258
|
+
if (typeof data.accessToken !== "string" || typeof data.refreshToken !== "string" || typeof data.expiresAt !== "number") return;
|
|
259
|
+
const scopes = Array.isArray(data.scopes) ? data.scopes.join(" ") : typeof data.scopes === "string" ? data.scopes : DEFAULT_SCOPES;
|
|
260
|
+
return {
|
|
261
|
+
accessToken: data.accessToken,
|
|
262
|
+
refreshToken: data.refreshToken,
|
|
263
|
+
expiresAt: Math.trunc(data.expiresAt),
|
|
264
|
+
scopes,
|
|
265
|
+
...typeof data.emailAddress === "string" ? { emailAddress: data.emailAddress } : {},
|
|
266
|
+
...typeof data.subscriptionType === "string" ? { subscriptionType: data.subscriptionType } : {}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function parseBlob(raw) {
|
|
270
|
+
let parsed;
|
|
271
|
+
try {
|
|
272
|
+
parsed = JSON.parse(raw);
|
|
273
|
+
} catch {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
return toSession(parsed.claudeAiOauth ?? parsed);
|
|
277
|
+
}
|
|
278
|
+
function credentialsFilePath() {
|
|
279
|
+
return join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"), ".credentials.json");
|
|
280
|
+
}
|
|
281
|
+
function readKeychainRaw() {
|
|
282
|
+
try {
|
|
283
|
+
return execFileSync("/usr/bin/security", [
|
|
284
|
+
"find-generic-password",
|
|
285
|
+
"-s",
|
|
286
|
+
PRIMARY_SERVICE,
|
|
287
|
+
"-w"
|
|
288
|
+
], {
|
|
289
|
+
timeout: 3e3,
|
|
290
|
+
encoding: "utf8",
|
|
291
|
+
stdio: [
|
|
292
|
+
"pipe",
|
|
293
|
+
"pipe",
|
|
294
|
+
"pipe"
|
|
295
|
+
]
|
|
296
|
+
}).trim();
|
|
297
|
+
} catch {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function readFileRaw() {
|
|
302
|
+
try {
|
|
303
|
+
return readFileSync(credentialsFilePath(), "utf8");
|
|
304
|
+
} catch {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/** Read the current Claude Code session from its source of truth: macOS Keychain, falling back to the credentials file. */
|
|
309
|
+
function readClaudeCodeCredentials() {
|
|
310
|
+
if (process.platform === "darwin") {
|
|
311
|
+
const raw$1 = readKeychainRaw();
|
|
312
|
+
const session = raw$1 !== void 0 ? parseBlob(raw$1) : void 0;
|
|
313
|
+
if (session) return session;
|
|
314
|
+
}
|
|
315
|
+
const raw = readFileRaw();
|
|
316
|
+
return raw !== void 0 ? parseBlob(raw) : void 0;
|
|
317
|
+
}
|
|
318
|
+
function blobMatches(raw, expectedAccessToken) {
|
|
319
|
+
return parseBlob(raw)?.accessToken === expectedAccessToken;
|
|
320
|
+
}
|
|
321
|
+
function getKeychainAccountName() {
|
|
322
|
+
try {
|
|
323
|
+
const output = execFileSync("/usr/bin/security", [
|
|
324
|
+
"find-generic-password",
|
|
325
|
+
"-s",
|
|
326
|
+
PRIMARY_SERVICE
|
|
327
|
+
], {
|
|
328
|
+
timeout: 2e3,
|
|
329
|
+
encoding: "utf8",
|
|
330
|
+
stdio: [
|
|
331
|
+
"pipe",
|
|
332
|
+
"pipe",
|
|
333
|
+
"pipe"
|
|
334
|
+
]
|
|
335
|
+
});
|
|
336
|
+
return /"acct"<blob>="([^"]*)"/.exec(output)?.[1];
|
|
337
|
+
} catch {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/** Merge fresh tokens into an existing raw blob, preserving unrelated fields. */
|
|
342
|
+
function mergeIntoBlob(existingRaw, next) {
|
|
343
|
+
let parsed;
|
|
344
|
+
try {
|
|
345
|
+
parsed = JSON.parse(existingRaw);
|
|
346
|
+
} catch {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const target = parsed.claudeAiOauth ?? parsed;
|
|
350
|
+
target.accessToken = next.accessToken;
|
|
351
|
+
target.refreshToken = next.refreshToken;
|
|
352
|
+
target.expiresAt = next.expiresAt;
|
|
353
|
+
return JSON.stringify(parsed);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Write a refreshed session back to Claude Code's own credential store, so
|
|
357
|
+
* the `claude` CLI and any other consumer of the same account see the token
|
|
358
|
+
* we just rotated. A stale-blob mismatch (something else rotated it first)
|
|
359
|
+
* is a no-op — the caller already has that other rotation via readClaudeCodeCredentials.
|
|
360
|
+
* @param next - the freshly refreshed session to persist.
|
|
361
|
+
* @param expectedPriorAccessToken - the access token this refresh started from.
|
|
362
|
+
* @returns whether the write-back succeeded.
|
|
363
|
+
*/
|
|
364
|
+
function writeBackClaudeCodeCredentials(next, expectedPriorAccessToken) {
|
|
365
|
+
if (process.platform === "darwin") {
|
|
366
|
+
const raw$1 = readKeychainRaw();
|
|
367
|
+
if (raw$1 === void 0 || !blobMatches(raw$1, expectedPriorAccessToken)) return false;
|
|
368
|
+
const updated$1 = mergeIntoBlob(raw$1, next);
|
|
369
|
+
if (updated$1 === void 0) return false;
|
|
370
|
+
const account = getKeychainAccountName() ?? PRIMARY_SERVICE;
|
|
371
|
+
try {
|
|
372
|
+
execFileSync("/usr/bin/security", [
|
|
373
|
+
"add-generic-password",
|
|
374
|
+
"-s",
|
|
375
|
+
PRIMARY_SERVICE,
|
|
376
|
+
"-a",
|
|
377
|
+
account,
|
|
378
|
+
"-w",
|
|
379
|
+
updated$1,
|
|
380
|
+
"-U"
|
|
381
|
+
], {
|
|
382
|
+
timeout: 2e3,
|
|
383
|
+
stdio: "ignore"
|
|
384
|
+
});
|
|
385
|
+
return true;
|
|
386
|
+
} catch {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const path = credentialsFilePath();
|
|
391
|
+
let raw;
|
|
392
|
+
try {
|
|
393
|
+
raw = readFileSync(path, "utf8");
|
|
394
|
+
} catch {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
if (!blobMatches(raw, expectedPriorAccessToken)) return false;
|
|
398
|
+
const updated = mergeIntoBlob(raw, next);
|
|
399
|
+
if (updated === void 0) return false;
|
|
400
|
+
try {
|
|
401
|
+
const dir = dirname(path);
|
|
402
|
+
if (!existsSync(dir)) mkdirSync(dir, {
|
|
403
|
+
recursive: true,
|
|
404
|
+
mode: 448
|
|
405
|
+
});
|
|
406
|
+
writeFileSync(path, updated, {
|
|
407
|
+
encoding: "utf8",
|
|
408
|
+
mode: 384
|
|
409
|
+
});
|
|
410
|
+
chmodSync(path, 384);
|
|
411
|
+
return true;
|
|
412
|
+
} catch {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Refresh a Claude session, first checking whether Claude Code's own store
|
|
418
|
+
* already holds a fresher token (rotated by the `claude` CLI or another
|
|
419
|
+
* consumer) before hitting the OAuth endpoint ourselves — and writing our own
|
|
420
|
+
* refresh back to that store so every consumer of the account stays synced.
|
|
421
|
+
* @param session - the session TokenManager wants refreshed.
|
|
422
|
+
* @param doRefresh - the actual OAuth refresh-token grant (network call).
|
|
423
|
+
* @returns the freshest available session.
|
|
424
|
+
*/
|
|
425
|
+
async function refreshClaudeSynced(session, doRefresh) {
|
|
426
|
+
const fromSource = readClaudeCodeCredentials();
|
|
427
|
+
const base = fromSource !== void 0 && fromSource.accessToken !== session.accessToken ? fromSource : session;
|
|
428
|
+
if (base.expiresAt > Date.now() + 6e4) return base;
|
|
429
|
+
const next = await doRefresh(base);
|
|
430
|
+
writeBackClaudeCodeCredentials(next, base.accessToken);
|
|
431
|
+
return next;
|
|
432
|
+
}
|
|
433
|
+
|
|
250
434
|
//#endregion
|
|
251
435
|
//#region src/auth/store.ts
|
|
252
436
|
/** Every provider route, in display order. */
|
|
@@ -827,13 +1011,16 @@ function sanitizeModel(value) {
|
|
|
827
1011
|
if (typeof raw.id !== "string" || raw.id.length === 0 || typeof raw.name !== "string" || raw.name.length === 0 || raw.description !== void 0 && typeof raw.description !== "string" || raw.contextWindow !== void 0 && (typeof raw.contextWindow !== "number" || !Number.isInteger(raw.contextWindow) || raw.contextWindow <= 0) || raw.priority !== void 0 && (typeof raw.priority !== "number" || !Number.isFinite(raw.priority))) return void 0;
|
|
828
1012
|
const reasoning = raw.reasoning === void 0 ? void 0 : sanitizeReasoning(raw.reasoning);
|
|
829
1013
|
if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
|
|
1014
|
+
const thinkingType = raw.thinkingType;
|
|
1015
|
+
if (thinkingType !== void 0 && thinkingType !== "enabled" && thinkingType !== "adaptive") return void 0;
|
|
830
1016
|
return {
|
|
831
1017
|
id: raw.id,
|
|
832
1018
|
name: raw.name,
|
|
833
1019
|
...raw.description === void 0 ? {} : { description: raw.description },
|
|
834
1020
|
...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
|
|
835
1021
|
...raw.priority === void 0 ? {} : { priority: raw.priority },
|
|
836
|
-
...reasoning === void 0 ? {} : { reasoning }
|
|
1022
|
+
...reasoning === void 0 ? {} : { reasoning },
|
|
1023
|
+
...thinkingType === void 0 ? {} : { thinkingType }
|
|
837
1024
|
};
|
|
838
1025
|
}
|
|
839
1026
|
/**
|
|
@@ -2143,12 +2330,11 @@ async function* streamAnthropic(stream, onActivity) {
|
|
|
2143
2330
|
//#endregion
|
|
2144
2331
|
//#region src/providers/claude.ts
|
|
2145
2332
|
const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
2146
|
-
const
|
|
2147
|
-
const CLAUDE_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
2333
|
+
const CLAUDE_TOKEN_URL = "https://claude.ai/v1/oauth/token";
|
|
2148
2334
|
const CLAUDE_API_URL = "https://api.anthropic.com/v1/messages?beta=true";
|
|
2149
2335
|
const CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
2336
|
+
const CLAUDE_MODELS_URL = "https://api.anthropic.com/v1/models?beta=true";
|
|
2150
2337
|
const CLAUDE_SCOPE = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
2151
|
-
const CLAUDE_CALLBACK_PATH = "/callback";
|
|
2152
2338
|
const CLAUDE_CONTEXT_WINDOW = 2e5;
|
|
2153
2339
|
const CLAUDE_DEFAULT_MAX_TOKENS = 32e3;
|
|
2154
2340
|
/** Refresh when the access token has less than this much life left. */
|
|
@@ -2158,28 +2344,28 @@ const CLAUDE_PREEMPT_MS = 5 * 6e4;
|
|
|
2158
2344
|
* so these headers impersonate the CLI; the harness attribution user-agent
|
|
2159
2345
|
* cannot be sent here (one user-agent slot, and the CLI's wins).
|
|
2160
2346
|
*/
|
|
2161
|
-
const
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2347
|
+
const CLAUDE_CLI_FALLBACK_VERSION = "2.1.234";
|
|
2348
|
+
function detectClaudeVersion() {
|
|
2349
|
+
try {
|
|
2350
|
+
const match = execFileSync("claude", ["--version"], {
|
|
2351
|
+
timeout: 3e3,
|
|
2352
|
+
encoding: "utf8"
|
|
2353
|
+
}).match(/^(\d+\.\d+\.\d+)/);
|
|
2354
|
+
if (match) return match[1];
|
|
2355
|
+
} catch {}
|
|
2356
|
+
return CLAUDE_CLI_FALLBACK_VERSION;
|
|
2357
|
+
}
|
|
2358
|
+
const CLAUDE_CLI_USER_AGENT = `claude-cli/${detectClaudeVersion()} (external, cli)`;
|
|
2359
|
+
const CLAUDE_BETA_FALLBACK = [
|
|
2360
|
+
"claude-code-20250219",
|
|
2361
|
+
"oauth-2025-04-20",
|
|
2362
|
+
"interleaved-thinking-2025-05-14",
|
|
2363
|
+
"context-management-2025-06-27",
|
|
2364
|
+
"effort-2025-11-24",
|
|
2365
|
+
"compact-2026-01-12",
|
|
2366
|
+
"files-api-2025-04-14"
|
|
2367
|
+
].join(",");
|
|
2368
|
+
const CLAUDE_BETA_FLAGS = CLAUDE_BETA_FALLBACK;
|
|
2183
2369
|
/** Best-effort account profile; login must not fail when this does. */
|
|
2184
2370
|
async function fetchClaudeProfile(accessToken) {
|
|
2185
2371
|
try {
|
|
@@ -2347,22 +2533,79 @@ async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
|
|
|
2347
2533
|
windows
|
|
2348
2534
|
};
|
|
2349
2535
|
}
|
|
2536
|
+
function claudeThinkingType(capabilities) {
|
|
2537
|
+
const types = capabilities?.thinking?.types;
|
|
2538
|
+
if (types?.enabled?.supported === true) return "enabled";
|
|
2539
|
+
if (types?.adaptive?.supported === true) return "adaptive";
|
|
2540
|
+
}
|
|
2541
|
+
/** Effort levels in display order; a model exposes only the ones it advertises as supported. */
|
|
2542
|
+
const CLAUDE_EFFORT_LEVELS = [
|
|
2543
|
+
"low",
|
|
2544
|
+
"medium",
|
|
2545
|
+
"high",
|
|
2546
|
+
"xhigh",
|
|
2547
|
+
"max"
|
|
2548
|
+
];
|
|
2549
|
+
function claudeReasoning(capabilities) {
|
|
2550
|
+
const effort = capabilities?.effort;
|
|
2551
|
+
if (effort?.supported !== true) return void 0;
|
|
2552
|
+
const efforts = CLAUDE_EFFORT_LEVELS.filter((level) => effort[level]?.supported === true).map((level) => ({
|
|
2553
|
+
id: ReasoningEffortId(level),
|
|
2554
|
+
name: level[0].toUpperCase() + level.slice(1)
|
|
2555
|
+
}));
|
|
2556
|
+
return efforts.length > 0 ? { efforts } : void 0;
|
|
2557
|
+
}
|
|
2558
|
+
/** Fetch the live model catalog from the subscription endpoint. */
|
|
2559
|
+
async function fetchClaudeModels(session, fetchFn = fetch) {
|
|
2560
|
+
const response = await fetchFn(CLAUDE_MODELS_URL, { headers: {
|
|
2561
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
2562
|
+
"anthropic-version": "2023-06-01",
|
|
2563
|
+
"user-agent": CLAUDE_CLI_USER_AGENT,
|
|
2564
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
2565
|
+
"accept": "application/json"
|
|
2566
|
+
} });
|
|
2567
|
+
if (!response.ok) throw await httpLlmError(response, "claude models API");
|
|
2568
|
+
const payload = await response.json();
|
|
2569
|
+
if (!Array.isArray(payload.data)) throw new Error("claude models API returned an invalid catalog");
|
|
2570
|
+
const models = payload.data.filter((m) => typeof m.id === "string").map((m) => {
|
|
2571
|
+
const thinkingType = claudeThinkingType(m.capabilities);
|
|
2572
|
+
const reasoning = claudeReasoning(m.capabilities);
|
|
2573
|
+
return {
|
|
2574
|
+
id: m.id,
|
|
2575
|
+
name: m.display_name ?? m.id,
|
|
2576
|
+
...thinkingType === void 0 ? {} : { thinkingType },
|
|
2577
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
2578
|
+
};
|
|
2579
|
+
});
|
|
2580
|
+
if (models.length === 0) throw new Error("claude models API returned an empty catalog");
|
|
2581
|
+
return models;
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Claude Code's own SDK retry shape: exponential backoff starting at 1s,
|
|
2585
|
+
* doubling per attempt, capped at 60s, plus jitter. `maxRetries` is the
|
|
2586
|
+
* count of retries after the first attempt (Claude Code defaults to 10).
|
|
2587
|
+
*/
|
|
2588
|
+
const CLAUDE_RETRY_INITIAL_DELAY_MS = 1e3;
|
|
2589
|
+
const CLAUDE_RETRY_MAX_DELAY_MS = 6e4;
|
|
2590
|
+
const CLAUDE_RETRY_JITTER_RATIO = .2;
|
|
2350
2591
|
/** The Claude 4.5 family accepts image input. */
|
|
2351
2592
|
const CLAUDE_MODALITIES = ["text", "image"];
|
|
2352
2593
|
/** Claude wire adapter: one instance serves the `claude` provider route. */
|
|
2353
2594
|
var ClaudeAdapter = class extends LlmAdapter {
|
|
2595
|
+
catalog;
|
|
2354
2596
|
constructor(options) {
|
|
2355
2597
|
super();
|
|
2356
2598
|
this.options = options;
|
|
2599
|
+
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
2357
2600
|
}
|
|
2358
|
-
|
|
2359
|
-
return
|
|
2360
|
-
id: provider,
|
|
2361
|
-
name: "Claude (Subscription)"
|
|
2362
|
-
};
|
|
2601
|
+
async fetchCatalog() {
|
|
2602
|
+
return fetchClaudeModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
2363
2603
|
}
|
|
2364
|
-
async
|
|
2365
|
-
if (!
|
|
2604
|
+
async discovered(model) {
|
|
2605
|
+
if (!this.options.discovery) return void 0;
|
|
2606
|
+
return (await this.catalog.resolve(() => this.fetchCatalog()))?.find((entry) => entry.id === model);
|
|
2607
|
+
}
|
|
2608
|
+
staticModels(provider) {
|
|
2366
2609
|
return this.options.models.map((model) => ({
|
|
2367
2610
|
provider,
|
|
2368
2611
|
id: model.id,
|
|
@@ -2370,16 +2613,54 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
2370
2613
|
inputModalities: model.inputModalities ?? CLAUDE_MODALITIES
|
|
2371
2614
|
}));
|
|
2372
2615
|
}
|
|
2373
|
-
|
|
2616
|
+
providerInfo(provider) {
|
|
2617
|
+
return {
|
|
2618
|
+
id: provider,
|
|
2619
|
+
name: "Claude (Subscription)"
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
providerRetryPolicy(provider) {
|
|
2623
|
+
if (this.options.maxRetries === void 0) return void 0;
|
|
2624
|
+
return resolveRetryPolicy({
|
|
2625
|
+
mode: "normal",
|
|
2626
|
+
maxRetries: this.options.maxRetries,
|
|
2627
|
+
backoff: {
|
|
2628
|
+
initialDelayMs: CLAUDE_RETRY_INITIAL_DELAY_MS,
|
|
2629
|
+
maxDelayMs: CLAUDE_RETRY_MAX_DELAY_MS,
|
|
2630
|
+
jitterRatio: CLAUDE_RETRY_JITTER_RATIO
|
|
2631
|
+
}
|
|
2632
|
+
}, `claude: provider "${provider}" retryPolicy`);
|
|
2633
|
+
}
|
|
2634
|
+
async listModels(provider) {
|
|
2635
|
+
if (await this.options.tokens.peek() === void 0) return [];
|
|
2636
|
+
if (!this.options.discovery) return this.staticModels(provider);
|
|
2637
|
+
try {
|
|
2638
|
+
return (await this.catalog.get(() => this.fetchCatalog())).map((model) => ({
|
|
2639
|
+
provider,
|
|
2640
|
+
id: model.id,
|
|
2641
|
+
name: model.name,
|
|
2642
|
+
inputModalities: CLAUDE_MODALITIES
|
|
2643
|
+
}));
|
|
2644
|
+
} catch (error) {
|
|
2645
|
+
if (error instanceof LlmError && (error.code === "MISSING_CREDENTIAL" || error.code === "INVALID_CREDENTIAL")) return [];
|
|
2646
|
+
if (error instanceof LlmError && error.code === "AUTH") this.catalog.invalidate();
|
|
2647
|
+
this.options.onWarn?.(`claude model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
2648
|
+
return this.staticModels(provider);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
async resolveModel(provider, model) {
|
|
2652
|
+
const disc = await this.discovered(model);
|
|
2374
2653
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
2375
|
-
|
|
2654
|
+
const reasoning = disc?.reasoning;
|
|
2655
|
+
return {
|
|
2376
2656
|
provider,
|
|
2377
2657
|
id: model,
|
|
2378
|
-
name: configured?.name ?? model,
|
|
2658
|
+
name: disc?.name ?? configured?.name ?? model,
|
|
2379
2659
|
inputModalities: configured?.inputModalities ?? CLAUDE_MODALITIES,
|
|
2380
|
-
context: { contextWindow: configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW },
|
|
2381
|
-
defaultMaxTokens: configured?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS
|
|
2382
|
-
|
|
2660
|
+
context: { contextWindow: disc?.contextWindow ?? configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW },
|
|
2661
|
+
defaultMaxTokens: configured?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS,
|
|
2662
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
2663
|
+
};
|
|
2383
2664
|
}
|
|
2384
2665
|
async *stream(options) {
|
|
2385
2666
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
@@ -2401,14 +2682,42 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
2401
2682
|
watchdog.stop();
|
|
2402
2683
|
}
|
|
2403
2684
|
}
|
|
2685
|
+
/**
|
|
2686
|
+
* `display: 'summarized'` is set explicitly on both shapes: `adaptive`-type
|
|
2687
|
+
* models default to `display: 'omitted'`, which returns thinking blocks with
|
|
2688
|
+
* an empty `thinking` field — without this override the "Think" panel would
|
|
2689
|
+
* always render empty even though real reasoning (and billed thinking_tokens)
|
|
2690
|
+
* ran.
|
|
2691
|
+
*/
|
|
2692
|
+
thinkingParam(thinkingType, maxTokens) {
|
|
2693
|
+
if (thinkingType === "adaptive") return {
|
|
2694
|
+
type: "adaptive",
|
|
2695
|
+
display: "summarized"
|
|
2696
|
+
};
|
|
2697
|
+
if (thinkingType === "enabled") {
|
|
2698
|
+
const budget = Math.min(Math.max(1024, Math.floor(maxTokens * .5)), maxTokens - 100);
|
|
2699
|
+
if (budget < 1024) return void 0;
|
|
2700
|
+
return {
|
|
2701
|
+
type: "enabled",
|
|
2702
|
+
budget_tokens: budget,
|
|
2703
|
+
display: "summarized"
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2404
2707
|
async request(options, session, signal) {
|
|
2405
2708
|
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
|
|
2709
|
+
const maxTokens = options.maxTokens ?? this.options.models.find((entry) => entry.id === options.model)?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS;
|
|
2710
|
+
const disc = await this.discovered(options.model);
|
|
2711
|
+
const thinking = this.thinkingParam(disc?.thinkingType, maxTokens);
|
|
2712
|
+
const effort = options.reasoningEffort !== void 0 && disc?.reasoning !== void 0 ? { output_config: { effort: String(options.reasoningEffort) } } : {};
|
|
2406
2713
|
const body = {
|
|
2407
2714
|
model: options.model,
|
|
2408
|
-
max_tokens:
|
|
2715
|
+
max_tokens: maxTokens,
|
|
2409
2716
|
system: toAnthropicSystem(options.system, messages),
|
|
2410
2717
|
messages: toAnthropicMessages(messages),
|
|
2411
2718
|
...options.tools !== void 0 && options.tools.length > 0 ? { tools: toAnthropicTools(options.tools) } : {},
|
|
2719
|
+
...thinking === void 0 ? {} : { thinking },
|
|
2720
|
+
...effort,
|
|
2412
2721
|
stream: true,
|
|
2413
2722
|
...options.sessionId !== void 0 ? { metadata: { user_id: String(options.sessionId) } } : {}
|
|
2414
2723
|
};
|
|
@@ -3697,17 +4006,27 @@ const DEFAULT_MODELS = {
|
|
|
3697
4006
|
],
|
|
3698
4007
|
claude: [
|
|
3699
4008
|
{
|
|
3700
|
-
id: "claude-opus-
|
|
3701
|
-
name: "Claude Opus
|
|
3702
|
-
maxTokens:
|
|
4009
|
+
id: "claude-opus-5",
|
|
4010
|
+
name: "Claude Opus 5",
|
|
4011
|
+
maxTokens: 128e3,
|
|
4012
|
+
contextWindow: 1e6
|
|
4013
|
+
},
|
|
4014
|
+
{
|
|
4015
|
+
id: "claude-sonnet-5",
|
|
4016
|
+
name: "Claude Sonnet 5",
|
|
4017
|
+
maxTokens: 128e3,
|
|
4018
|
+
contextWindow: 1e6
|
|
3703
4019
|
},
|
|
3704
4020
|
{
|
|
3705
|
-
id: "claude-
|
|
3706
|
-
name: "Claude
|
|
4021
|
+
id: "claude-fable-5",
|
|
4022
|
+
name: "Claude Fable 5",
|
|
4023
|
+
maxTokens: 128e3,
|
|
4024
|
+
contextWindow: 1e6
|
|
3707
4025
|
},
|
|
3708
4026
|
{
|
|
3709
|
-
id: "claude-haiku-4-5",
|
|
3710
|
-
name: "Claude Haiku 4.5"
|
|
4027
|
+
id: "claude-haiku-4-5-20251001",
|
|
4028
|
+
name: "Claude Haiku 4.5",
|
|
4029
|
+
maxTokens: 64e3
|
|
3711
4030
|
}
|
|
3712
4031
|
],
|
|
3713
4032
|
grok: [
|
|
@@ -3796,7 +4115,17 @@ var SubscriptionsAuthController = class {
|
|
|
3796
4115
|
};
|
|
3797
4116
|
}
|
|
3798
4117
|
async login(provider) {
|
|
3799
|
-
|
|
4118
|
+
if (provider === "claude") {
|
|
4119
|
+
const session = readClaudeCodeCredentials();
|
|
4120
|
+
if (session) {
|
|
4121
|
+
await this.persist("claude", session);
|
|
4122
|
+
this.lastError.delete("claude");
|
|
4123
|
+
this.onAuthChanged("claude");
|
|
4124
|
+
return { authorizeUrl: "" };
|
|
4125
|
+
}
|
|
4126
|
+
throw new Error("Claude Code credentials not found. Run \"claude\" first to log in.");
|
|
4127
|
+
}
|
|
4128
|
+
const spec = provider === "grok" ? await grokFlow() : codexFlow;
|
|
3800
4129
|
const attempt = await this.flows.start(provider, spec);
|
|
3801
4130
|
this.complete(provider, attempt);
|
|
3802
4131
|
return { authorizeUrl: attempt.authorizeUrl };
|
|
@@ -3860,6 +4189,7 @@ function apply(ctx, config) {
|
|
|
3860
4189
|
handles.get(provider)?.replace([provider]);
|
|
3861
4190
|
};
|
|
3862
4191
|
let codexTokens;
|
|
4192
|
+
let claudeTokens;
|
|
3863
4193
|
let grokTokens;
|
|
3864
4194
|
const usageFetchers = {};
|
|
3865
4195
|
for (const provider of providers) switch (provider) {
|
|
@@ -3896,18 +4226,23 @@ function apply(ctx, config) {
|
|
|
3896
4226
|
load: () => getSession("claude"),
|
|
3897
4227
|
save: (session) => saveSession("claude", session),
|
|
3898
4228
|
remove: () => deleteSession("claude"),
|
|
3899
|
-
refresh: refreshClaude,
|
|
4229
|
+
refresh: (session) => refreshClaudeSynced(session, refreshClaude),
|
|
3900
4230
|
isPermanent: isClaudePermanentRefreshError,
|
|
3901
4231
|
onRemoved: () => {
|
|
3902
4232
|
authChanged("claude");
|
|
3903
4233
|
}
|
|
3904
4234
|
});
|
|
4235
|
+
claudeTokens = tokens;
|
|
3905
4236
|
usageFetchers.claude = async (signal) => fetchClaudeUsage(await tokens.session(), fetch, signal);
|
|
3906
4237
|
handles.set("claude", ctx.llm.registerAdapter(["claude"], new ClaudeAdapter({
|
|
3907
4238
|
models: catalog.claude,
|
|
3908
4239
|
streamIdleTimeoutMs,
|
|
3909
4240
|
tokens,
|
|
3910
|
-
|
|
4241
|
+
discovery: !overridden.has("claude"),
|
|
4242
|
+
onWarn,
|
|
4243
|
+
maxRetries: 10,
|
|
4244
|
+
resolveAttachments,
|
|
4245
|
+
catalogStore: catalogStore("claude")
|
|
3911
4246
|
})));
|
|
3912
4247
|
break;
|
|
3913
4248
|
}
|
|
@@ -3939,6 +4274,14 @@ function apply(ctx, config) {
|
|
|
3939
4274
|
}
|
|
3940
4275
|
}
|
|
3941
4276
|
registerAuthRpc(ctx, new SubscriptionsAuthController(flows, authChanged, resolveAttachments, usageFetchers));
|
|
4277
|
+
if (claudeTokens !== void 0) {
|
|
4278
|
+
const syncTimer = setInterval(() => {
|
|
4279
|
+
claudeTokens?.session().catch(() => {});
|
|
4280
|
+
}, 5 * 6e4);
|
|
4281
|
+
ctx.effect(() => () => {
|
|
4282
|
+
clearInterval(syncTimer);
|
|
4283
|
+
}, "dsh-plugin-subscriptions: claude background sync timer");
|
|
4284
|
+
}
|
|
3942
4285
|
ctx.inject(["tools"], (toolsCtx) => {
|
|
3943
4286
|
if (grokTokens !== void 0) {
|
|
3944
4287
|
toolsCtx.tools.register(createXSearchTool({ tokens: grokTokens }));
|
|
@@ -71,6 +71,9 @@ function sanitizeModel(value) {
|
|
|
71
71
|
const reasoning = raw.reasoning === undefined ? undefined : sanitizeReasoning(raw.reasoning);
|
|
72
72
|
if (raw.reasoning !== undefined && reasoning === undefined)
|
|
73
73
|
return undefined;
|
|
74
|
+
const thinkingType = raw.thinkingType;
|
|
75
|
+
if (thinkingType !== undefined && thinkingType !== 'enabled' && thinkingType !== 'adaptive')
|
|
76
|
+
return undefined;
|
|
74
77
|
return {
|
|
75
78
|
id: raw.id,
|
|
76
79
|
name: raw.name,
|
|
@@ -78,6 +81,7 @@ function sanitizeModel(value) {
|
|
|
78
81
|
...raw.contextWindow === undefined ? {} : { contextWindow: raw.contextWindow },
|
|
79
82
|
...raw.priority === undefined ? {} : { priority: raw.priority },
|
|
80
83
|
...reasoning === undefined ? {} : { reasoning },
|
|
84
|
+
...thinkingType === undefined ? {} : { thinkingType: thinkingType },
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
87
|
/**
|
|
@@ -9,14 +9,23 @@ import type { FlowSpec } from '../auth/oauth-flow.js';
|
|
|
9
9
|
import type { ClaudeSession } from '../auth/store.js';
|
|
10
10
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
11
11
|
import { TokenManager } from './common.js';
|
|
12
|
-
import type { FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
12
|
+
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
13
13
|
export declare const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
14
|
-
export declare const CLAUDE_AUTHORIZE_URL = "https://claude.
|
|
15
|
-
export declare const CLAUDE_TOKEN_URL = "https://
|
|
14
|
+
export declare const CLAUDE_AUTHORIZE_URL = "https://claude.com/cai/oauth/authorize";
|
|
15
|
+
export declare const CLAUDE_TOKEN_URL = "https://claude.ai/v1/oauth/token";
|
|
16
16
|
export declare const CLAUDE_API_URL = "https://api.anthropic.com/v1/messages?beta=true";
|
|
17
17
|
export declare const CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
18
|
+
export declare const CLAUDE_MODELS_URL = "https://api.anthropic.com/v1/models?beta=true";
|
|
18
19
|
/** Refresh when the access token has less than this much life left. */
|
|
19
20
|
export declare const CLAUDE_PREEMPT_MS: number;
|
|
21
|
+
/**
|
|
22
|
+
* The subscription endpoint only serves requests presenting as Claude Code,
|
|
23
|
+
* so these headers impersonate the CLI; the harness attribution user-agent
|
|
24
|
+
* cannot be sent here (one user-agent slot, and the CLI's wins).
|
|
25
|
+
*/
|
|
26
|
+
export declare const CLAUDE_CLI_FALLBACK_VERSION = "2.1.234";
|
|
27
|
+
export declare function detectClaudeVersion(): string;
|
|
28
|
+
export declare const CLAUDE_BETA_FALLBACK: string;
|
|
20
29
|
/** Static claude flow facts for the OAuth flow engine. */
|
|
21
30
|
export declare const claudeFlow: FlowSpec;
|
|
22
31
|
/**
|
|
@@ -52,21 +61,44 @@ export declare const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usa
|
|
|
52
61
|
* @returns the mapped usage snapshot.
|
|
53
62
|
*/
|
|
54
63
|
export declare function fetchClaudeUsage(session: ClaudeSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
64
|
+
/** Fetch the live model catalog from the subscription endpoint. */
|
|
65
|
+
export declare function fetchClaudeModels(session: ClaudeSession, fetchFn?: FetchFn): Promise<DiscoveredModel[]>;
|
|
55
66
|
/** Constructor dependencies for {@link ClaudeAdapter}. */
|
|
56
67
|
export interface ClaudeAdapterOptions {
|
|
57
68
|
models: readonly ModelEntry[];
|
|
58
69
|
streamIdleTimeoutMs: number;
|
|
59
70
|
tokens: TokenManager<ClaudeSession>;
|
|
71
|
+
/** Whether to fetch the live catalog when logged in (false when config `models` overrides). */
|
|
72
|
+
discovery: boolean;
|
|
73
|
+
fetchFn?: FetchFn;
|
|
74
|
+
onWarn?: (message: string) => void;
|
|
75
|
+
/** Max retries on a retryable failure before giving up; matches Claude Code's own client-side retry count. Defaults to the dsh-llm default (2) when unset. */
|
|
76
|
+
maxRetries?: number;
|
|
60
77
|
/** Resolve the attachment service per request; absent means image requests fail loudly. */
|
|
61
78
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
79
|
+
/** Durable catalog store seeding capability metadata across restarts. */
|
|
80
|
+
catalogStore?: CatalogPersistence;
|
|
62
81
|
}
|
|
63
82
|
/** Claude wire adapter: one instance serves the `claude` provider route. */
|
|
64
83
|
export declare class ClaudeAdapter extends LlmAdapter {
|
|
65
84
|
private readonly options;
|
|
85
|
+
private readonly catalog;
|
|
66
86
|
constructor(options: ClaudeAdapterOptions);
|
|
87
|
+
private fetchCatalog;
|
|
88
|
+
private discovered;
|
|
89
|
+
private staticModels;
|
|
67
90
|
providerInfo(provider: string): LlmProviderInfo;
|
|
91
|
+
providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy | undefined;
|
|
68
92
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
69
93
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
70
94
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
95
|
+
/**
|
|
96
|
+
* `display: 'summarized'` is set explicitly on both shapes: `adaptive`-type
|
|
97
|
+
* models default to `display: 'omitted'`, which returns thinking blocks with
|
|
98
|
+
* an empty `thinking` field — without this override the "Think" panel would
|
|
99
|
+
* always render empty even though real reasoning (and billed thinking_tokens)
|
|
100
|
+
* ran.
|
|
101
|
+
*/
|
|
102
|
+
private thinkingParam;
|
|
71
103
|
private request;
|
|
72
104
|
}
|