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/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 = 16) {
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 CLAUDE_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
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 CLAUDE_CLI_USER_AGENT = "claude-cli/2.1.97 (external, cli)";
2162
- const CLAUDE_BETA_FLAGS = "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27";
2163
- /** Static claude flow facts for the OAuth flow engine. */
2164
- const claudeFlow = {
2165
- callbackPath: CLAUDE_CALLBACK_PATH,
2166
- listen: {
2167
- host: "localhost",
2168
- ports: [0]
2169
- },
2170
- buildAuthorizeUrl({ redirectUri, state, pkce }) {
2171
- return `${CLAUDE_AUTHORIZE_URL}?${new URLSearchParams({
2172
- code: "true",
2173
- client_id: CLAUDE_CLIENT_ID,
2174
- response_type: "code",
2175
- redirect_uri: redirectUri,
2176
- scope: CLAUDE_SCOPE,
2177
- code_challenge: pkce.challenge,
2178
- code_challenge_method: "S256",
2179
- state
2180
- }).toString()}`;
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
- providerInfo(provider) {
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 listModels(provider) {
2365
- if (!await this.options.tokens.hasSession()) return [];
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
- resolveModel(provider, model) {
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
- return Promise.resolve({
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: options.maxTokens ?? this.options.models.find((entry) => entry.id === options.model)?.maxTokens ?? CLAUDE_DEFAULT_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-4-5",
3701
- name: "Claude Opus 4.5",
3702
- maxTokens: 64e3
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-sonnet-4-5",
3706
- name: "Claude Sonnet 4.5"
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
- const spec = provider === "grok" ? await grokFlow() : provider === "claude" ? claudeFlow : codexFlow;
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
- resolveAttachments
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.ai/oauth/authorize";
15
- export declare const CLAUDE_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
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
  }