dsh-plugin-subscriptions 0.4.1 → 0.5.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.
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, 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. */
@@ -411,6 +595,12 @@ function readString(payload, field) {
411
595
  if (typeof value !== "string" || value.length === 0) throw new BadRequest(`payload.${field} must be a non-empty string`);
412
596
  return value;
413
597
  }
598
+ /** Validate the `setSpeed` endpoint's tier. */
599
+ function readSpeedTier(payload) {
600
+ const tier = payload.tier;
601
+ if (tier !== "standard" && tier !== "fast") throw new BadRequest("payload.tier must be \"standard\" or \"fast\"");
602
+ return tier;
603
+ }
414
604
  /** Validate the `image` endpoint's payload into a full attachment reference. */
415
605
  function readImageRef(payload) {
416
606
  if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
@@ -449,7 +639,12 @@ function readVideoName(payload) {
449
639
  if (typeof name$1 !== "string" || !VIDEO_NAME_PATTERN.test(name$1)) throw new BadRequest("payload.name must be a bare .mp4 file name");
450
640
  return name$1;
451
641
  }
452
- async function dispatch(controller, endpoint, payload, signal) {
642
+ /** Validate the session id both speed endpoints carry. */
643
+ function readSessionId(payload) {
644
+ if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
645
+ return readString(payload, "sessionId");
646
+ }
647
+ async function dispatch(controller, speed, endpoint, payload, signal) {
453
648
  switch (endpoint) {
454
649
  case "status": {
455
650
  const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider)]));
@@ -470,6 +665,10 @@ async function dispatch(controller, endpoint, payload, signal) {
470
665
  case "usage": return ok(await controller.usage(readProvider(payload), signal));
471
666
  case "image": return ok(await controller.readImage(readImageRef(payload), signal));
472
667
  case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
668
+ case "speed": return ok(await speed.speed(readSessionId(payload)));
669
+ case "setSpeed":
670
+ await speed.setSpeed(readSessionId(payload), readSpeedTier(payload));
671
+ return ok({ ok: true });
473
672
  default: throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
474
673
  }
475
674
  }
@@ -477,13 +676,14 @@ async function dispatch(controller, endpoint, payload, signal) {
477
676
  * Register the `/subscriptions-auth` RPC channel when a host connection exists.
478
677
  * @param ctx - the plugin context (headless profiles have no `connection`).
479
678
  * @param controller - the auth operations backing the endpoints.
679
+ * @param speed - the per-session speed-tier state backing the Speed toggle.
480
680
  */
481
- function registerAuthRpc(ctx, controller) {
681
+ function registerAuthRpc(ctx, controller, speed) {
482
682
  ctx.inject(["connection"], (ctx$1) => {
483
683
  const connection = ctx$1.get("connection");
484
684
  ctx$1.effect(() => connection.rpc.handle(SUBSCRIPTIONS_AUTH_CHANNEL, async (endpoint, payload, signal) => {
485
685
  try {
486
- return await dispatch(controller, endpoint, payload, signal);
686
+ return await dispatch(controller, speed, endpoint, payload, signal);
487
687
  } catch (error) {
488
688
  return failure(error);
489
689
  }
@@ -827,13 +1027,19 @@ function sanitizeModel(value) {
827
1027
  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
1028
  const reasoning = raw.reasoning === void 0 ? void 0 : sanitizeReasoning(raw.reasoning);
829
1029
  if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
1030
+ const thinkingType = raw.thinkingType;
1031
+ if (thinkingType !== void 0 && thinkingType !== "enabled" && thinkingType !== "adaptive") return void 0;
1032
+ const fastTier = raw.fastTier;
1033
+ if (fastTier !== void 0 && typeof fastTier !== "boolean") return void 0;
830
1034
  return {
831
1035
  id: raw.id,
832
1036
  name: raw.name,
833
1037
  ...raw.description === void 0 ? {} : { description: raw.description },
834
1038
  ...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
835
1039
  ...raw.priority === void 0 ? {} : { priority: raw.priority },
836
- ...reasoning === void 0 ? {} : { reasoning }
1040
+ ...reasoning === void 0 ? {} : { reasoning },
1041
+ ...thinkingType === void 0 ? {} : { thinkingType },
1042
+ ...fastTier === void 0 ? {} : { fastTier }
837
1043
  };
838
1044
  }
839
1045
  /**
@@ -1397,6 +1603,14 @@ const CODEX_EFFORTS = [
1397
1603
  const CODEX_DEFAULT_EFFORT = ReasoningEffortId("high");
1398
1604
  /** Every gpt-5.x codex model accepts image input. */
1399
1605
  const CODEX_MODALITIES = ["text", "image"];
1606
+ /**
1607
+ * Fast tier (the codex CLI's "fast mode"): the Responses `service_tier` wire
1608
+ * value for priority processing, mirroring codex-rs
1609
+ * `ServiceTier::Fast.request_value()`. The legacy catalog spelling is the
1610
+ * `additional_speed_tiers` entry "fast".
1611
+ */
1612
+ const CODEX_FAST_SERVICE_TIER = "priority";
1613
+ const CODEX_FAST_SPEED_TIER = "fast";
1400
1614
  /** Static codex flow facts for the OAuth flow engine. */
1401
1615
  const codexFlow = {
1402
1616
  callbackPath: CODEX_CALLBACK_PATH,
@@ -1608,6 +1822,14 @@ function effortName(effort) {
1608
1822
  return effort === "xhigh" ? "Extra High" : effort.charAt(0).toUpperCase() + effort.slice(1);
1609
1823
  }
1610
1824
  /**
1825
+ * Whether a catalog entry advertises the fast tier. Mirrors codex-rs
1826
+ * `ModelPreset::supports_fast_mode`: a `service_tiers` id matching the fast
1827
+ * wire value, or the legacy `additional_speed_tiers` "fast" entry.
1828
+ */
1829
+ function supportsFastTier(entry) {
1830
+ return (entry.service_tiers ?? []).some((tier) => tier.id === CODEX_FAST_SERVICE_TIER) || (entry.additional_speed_tiers ?? []).includes(CODEX_FAST_SPEED_TIER);
1831
+ }
1832
+ /**
1611
1833
  * Fetch the live codex model catalog with the session's auth headers.
1612
1834
  * @param session - the stored session (used as-is; never refreshed here).
1613
1835
  * @param fetchFn - fetch implementation (injectable for tests).
@@ -1634,7 +1856,7 @@ async function fetchCodexModels(session, fetchFn = fetch) {
1634
1856
  ...level.description === void 0 ? {} : { description: level.description }
1635
1857
  }));
1636
1858
  const defaultEffort = typeof entry.default_reasoning_level === "string" && entry.default_reasoning_level.length > 0 && efforts.some((effort) => effort.id === ReasoningEffortId(entry.default_reasoning_level)) ? ReasoningEffortId(entry.default_reasoning_level) : void 0;
1637
- discovered.push({
1859
+ const model = {
1638
1860
  id: entry.slug,
1639
1861
  name: typeof entry.display_name === "string" && entry.display_name.length > 0 ? entry.display_name : entry.slug,
1640
1862
  ...typeof entry.description === "string" && entry.description.length > 0 ? { description: entry.description } : {},
@@ -1643,13 +1865,40 @@ async function fetchCodexModels(session, fetchFn = fetch) {
1643
1865
  ...efforts.length > 0 ? { reasoning: {
1644
1866
  efforts,
1645
1867
  ...defaultEffort === void 0 ? {} : { defaultEffort }
1646
- } } : {}
1647
- });
1868
+ } } : {},
1869
+ ...supportsFastTier(entry) ? { fastTier: true } : {}
1870
+ };
1871
+ discovered.push(model);
1648
1872
  }
1649
1873
  discovered.sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));
1650
1874
  if (discovered.length === 0) throw new Error(`codex models endpoint returned an empty catalog (client_version ${CODEX_CLIENT_VERSION})`);
1651
1875
  return discovered;
1652
1876
  }
1877
+ /**
1878
+ * The Responses request body for one generation. A fast-tier request (the
1879
+ * composer Speed toggle, the codex CLI's fast mode) carries
1880
+ * `service_tier: priority`; the tier field is omitted entirely otherwise,
1881
+ * matching the CLI (it never sends an explicit standard tier).
1882
+ */
1883
+ function codexRequestBody(options, resolved, fast) {
1884
+ return {
1885
+ model: options.model,
1886
+ instructions: resolved.instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
1887
+ input: resolved.input,
1888
+ ...options.tools !== void 0 && options.tools.length > 0 ? { tools: toResponsesTools(options.tools) } : {},
1889
+ tool_choice: "auto",
1890
+ parallel_tool_calls: true,
1891
+ ...options.reasoningEffort !== void 0 ? { reasoning: {
1892
+ effort: String(options.reasoningEffort),
1893
+ summary: "auto"
1894
+ } } : {},
1895
+ store: false,
1896
+ stream: true,
1897
+ include: ["reasoning.encrypted_content"],
1898
+ ...options.sessionId !== void 0 ? { prompt_cache_key: String(options.sessionId) } : {},
1899
+ ...fast ? { service_tier: CODEX_FAST_SERVICE_TIER } : {}
1900
+ };
1901
+ }
1653
1902
  /** Codex wire adapter: one instance serves the `codex` provider route. */
1654
1903
  var CodexAdapter = class extends LlmAdapter {
1655
1904
  catalog;
@@ -1705,6 +1954,16 @@ var CodexAdapter = class extends LlmAdapter {
1705
1954
  if (!this.options.discovery) return void 0;
1706
1955
  return (await this.catalog.resolve(() => this.fetchCatalog()))?.find((entry) => entry.id === model);
1707
1956
  }
1957
+ /** Whether the discovered catalog advertises a fast tier for this model. */
1958
+ async supportsFastTier(model) {
1959
+ return (await this.discovered(model))?.fastTier === true;
1960
+ }
1961
+ /** Ids of every discovered model with a fast tier (the Speed toggle's visibility list). */
1962
+ async fastCapableModels() {
1963
+ if (!this.options.discovery) return [];
1964
+ if (await this.options.tokens.peek() === void 0) return [];
1965
+ return (await this.catalog.resolve(() => this.fetchCatalog()) ?? []).filter((model) => model.fastTier === true).map((model) => model.id);
1966
+ }
1708
1967
  async resolveModel(provider, model) {
1709
1968
  const discovered = await this.discovered(model);
1710
1969
  const configured = this.options.models.find((entry) => entry.id === model);
@@ -1743,23 +2002,9 @@ var CodexAdapter = class extends LlmAdapter {
1743
2002
  }
1744
2003
  }
1745
2004
  async request(options, session, signal) {
1746
- const { instructions, input } = toResponsesInput(await resolveImages(options.messages, this.options.resolveAttachments?.(), signal), options.system);
1747
- const body = {
1748
- model: options.model,
1749
- instructions: instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
1750
- input,
1751
- ...options.tools !== void 0 && options.tools.length > 0 ? { tools: toResponsesTools(options.tools) } : {},
1752
- tool_choice: "auto",
1753
- parallel_tool_calls: true,
1754
- ...options.reasoningEffort !== void 0 ? { reasoning: {
1755
- effort: String(options.reasoningEffort),
1756
- summary: "auto"
1757
- } } : {},
1758
- store: false,
1759
- stream: true,
1760
- include: ["reasoning.encrypted_content"],
1761
- ...options.sessionId !== void 0 ? { prompt_cache_key: String(options.sessionId) } : {}
1762
- };
2005
+ const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
2006
+ const fast = this.options.speedFor !== void 0 && await this.options.speedFor(options.sessionId, options.model);
2007
+ const body = codexRequestBody(options, toResponsesInput(messages, options.system), fast);
1763
2008
  return fetch(CODEX_API_URL, {
1764
2009
  method: "POST",
1765
2010
  headers: {
@@ -2143,12 +2388,11 @@ async function* streamAnthropic(stream, onActivity) {
2143
2388
  //#endregion
2144
2389
  //#region src/providers/claude.ts
2145
2390
  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";
2391
+ const CLAUDE_TOKEN_URL = "https://claude.ai/v1/oauth/token";
2148
2392
  const CLAUDE_API_URL = "https://api.anthropic.com/v1/messages?beta=true";
2149
2393
  const CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
2394
+ const CLAUDE_MODELS_URL = "https://api.anthropic.com/v1/models?beta=true";
2150
2395
  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
2396
  const CLAUDE_CONTEXT_WINDOW = 2e5;
2153
2397
  const CLAUDE_DEFAULT_MAX_TOKENS = 32e3;
2154
2398
  /** Refresh when the access token has less than this much life left. */
@@ -2158,28 +2402,32 @@ const CLAUDE_PREEMPT_MS = 5 * 6e4;
2158
2402
  * so these headers impersonate the CLI; the harness attribution user-agent
2159
2403
  * cannot be sent here (one user-agent slot, and the CLI's wins).
2160
2404
  */
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
- };
2405
+ const CLAUDE_CLI_FALLBACK_VERSION = "2.1.234";
2406
+ function detectClaudeVersion() {
2407
+ try {
2408
+ const match = execFileSync("claude", ["--version"], {
2409
+ timeout: 3e3,
2410
+ encoding: "utf8"
2411
+ }).match(/^(\d+\.\d+\.\d+)/);
2412
+ if (match) return match[1];
2413
+ } catch {}
2414
+ return CLAUDE_CLI_FALLBACK_VERSION;
2415
+ }
2416
+ let claudeCliUserAgent;
2417
+ function getClaudeCliUserAgent() {
2418
+ if (claudeCliUserAgent === void 0) claudeCliUserAgent = `claude-cli/${detectClaudeVersion()} (external, cli)`;
2419
+ return claudeCliUserAgent;
2420
+ }
2421
+ const CLAUDE_BETA_FALLBACK = [
2422
+ "claude-code-20250219",
2423
+ "oauth-2025-04-20",
2424
+ "interleaved-thinking-2025-05-14",
2425
+ "context-management-2025-06-27",
2426
+ "effort-2025-11-24",
2427
+ "compact-2026-01-12",
2428
+ "files-api-2025-04-14"
2429
+ ].join(",");
2430
+ const CLAUDE_BETA_FLAGS = CLAUDE_BETA_FALLBACK;
2183
2431
  /** Best-effort account profile; login must not fail when this does. */
2184
2432
  async function fetchClaudeProfile(accessToken) {
2185
2433
  try {
@@ -2322,7 +2570,7 @@ async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
2322
2570
  headers: {
2323
2571
  "authorization": `Bearer ${session.accessToken}`,
2324
2572
  "anthropic-beta": "oauth-2025-04-20",
2325
- "user-agent": CLAUDE_CLI_USER_AGENT,
2573
+ "user-agent": getClaudeCliUserAgent(),
2326
2574
  "accept": "application/json"
2327
2575
  },
2328
2576
  ...signal === void 0 ? {} : { signal }
@@ -2347,22 +2595,79 @@ async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
2347
2595
  windows
2348
2596
  };
2349
2597
  }
2598
+ function claudeThinkingType(capabilities) {
2599
+ const types = capabilities?.thinking?.types;
2600
+ if (types?.enabled?.supported === true) return "enabled";
2601
+ if (types?.adaptive?.supported === true) return "adaptive";
2602
+ }
2603
+ /** Effort levels in display order; a model exposes only the ones it advertises as supported. */
2604
+ const CLAUDE_EFFORT_LEVELS = [
2605
+ "low",
2606
+ "medium",
2607
+ "high",
2608
+ "xhigh",
2609
+ "max"
2610
+ ];
2611
+ function claudeReasoning(capabilities) {
2612
+ const effort = capabilities?.effort;
2613
+ if (effort?.supported !== true) return void 0;
2614
+ const efforts = CLAUDE_EFFORT_LEVELS.filter((level) => effort[level]?.supported === true).map((level) => ({
2615
+ id: ReasoningEffortId(level),
2616
+ name: level[0].toUpperCase() + level.slice(1)
2617
+ }));
2618
+ return efforts.length > 0 ? { efforts } : void 0;
2619
+ }
2620
+ /** Fetch the live model catalog from the subscription endpoint. */
2621
+ async function fetchClaudeModels(session, fetchFn = fetch) {
2622
+ const response = await fetchFn(CLAUDE_MODELS_URL, { headers: {
2623
+ "authorization": `Bearer ${session.accessToken}`,
2624
+ "anthropic-version": "2023-06-01",
2625
+ "user-agent": getClaudeCliUserAgent(),
2626
+ "anthropic-dangerous-direct-browser-access": "true",
2627
+ "accept": "application/json"
2628
+ } });
2629
+ if (!response.ok) throw await httpLlmError(response, "claude models API");
2630
+ const payload = await response.json();
2631
+ if (!Array.isArray(payload.data)) throw new Error("claude models API returned an invalid catalog");
2632
+ const models = payload.data.filter((m) => typeof m.id === "string").map((m) => {
2633
+ const thinkingType = claudeThinkingType(m.capabilities);
2634
+ const reasoning = claudeReasoning(m.capabilities);
2635
+ return {
2636
+ id: m.id,
2637
+ name: m.display_name ?? m.id,
2638
+ ...thinkingType === void 0 ? {} : { thinkingType },
2639
+ ...reasoning === void 0 ? {} : { reasoning }
2640
+ };
2641
+ });
2642
+ if (models.length === 0) throw new Error("claude models API returned an empty catalog");
2643
+ return models;
2644
+ }
2645
+ /**
2646
+ * Claude Code's own SDK retry shape: exponential backoff starting at 1s,
2647
+ * doubling per attempt, capped at 60s, plus jitter. `maxRetries` is the
2648
+ * count of retries after the first attempt (Claude Code defaults to 10).
2649
+ */
2650
+ const CLAUDE_RETRY_INITIAL_DELAY_MS = 1e3;
2651
+ const CLAUDE_RETRY_MAX_DELAY_MS = 6e4;
2652
+ const CLAUDE_RETRY_JITTER_RATIO = .2;
2350
2653
  /** The Claude 4.5 family accepts image input. */
2351
2654
  const CLAUDE_MODALITIES = ["text", "image"];
2352
2655
  /** Claude wire adapter: one instance serves the `claude` provider route. */
2353
2656
  var ClaudeAdapter = class extends LlmAdapter {
2657
+ catalog;
2354
2658
  constructor(options) {
2355
2659
  super();
2356
2660
  this.options = options;
2661
+ this.catalog = new ModelCatalogCache(options.catalogStore);
2357
2662
  }
2358
- providerInfo(provider) {
2359
- return {
2360
- id: provider,
2361
- name: "Claude (Subscription)"
2362
- };
2663
+ async fetchCatalog() {
2664
+ return fetchClaudeModels(await this.options.tokens.session(), this.options.fetchFn);
2363
2665
  }
2364
- async listModels(provider) {
2365
- if (!await this.options.tokens.hasSession()) return [];
2666
+ async discovered(model) {
2667
+ if (!this.options.discovery) return void 0;
2668
+ return (await this.catalog.resolve(() => this.fetchCatalog()))?.find((entry) => entry.id === model);
2669
+ }
2670
+ staticModels(provider) {
2366
2671
  return this.options.models.map((model) => ({
2367
2672
  provider,
2368
2673
  id: model.id,
@@ -2370,16 +2675,54 @@ var ClaudeAdapter = class extends LlmAdapter {
2370
2675
  inputModalities: model.inputModalities ?? CLAUDE_MODALITIES
2371
2676
  }));
2372
2677
  }
2373
- resolveModel(provider, model) {
2678
+ providerInfo(provider) {
2679
+ return {
2680
+ id: provider,
2681
+ name: "Claude (Subscription)"
2682
+ };
2683
+ }
2684
+ providerRetryPolicy(provider) {
2685
+ if (this.options.maxRetries === void 0) return void 0;
2686
+ return resolveRetryPolicy({
2687
+ mode: "normal",
2688
+ maxRetries: this.options.maxRetries,
2689
+ backoff: {
2690
+ initialDelayMs: CLAUDE_RETRY_INITIAL_DELAY_MS,
2691
+ maxDelayMs: CLAUDE_RETRY_MAX_DELAY_MS,
2692
+ jitterRatio: CLAUDE_RETRY_JITTER_RATIO
2693
+ }
2694
+ }, `claude: provider "${provider}" retryPolicy`);
2695
+ }
2696
+ async listModels(provider) {
2697
+ if (await this.options.tokens.peek() === void 0) return [];
2698
+ if (!this.options.discovery) return this.staticModels(provider);
2699
+ try {
2700
+ return (await this.catalog.get(() => this.fetchCatalog())).map((model) => ({
2701
+ provider,
2702
+ id: model.id,
2703
+ name: model.name,
2704
+ inputModalities: CLAUDE_MODALITIES
2705
+ }));
2706
+ } catch (error) {
2707
+ if (error instanceof LlmError && (error.code === "MISSING_CREDENTIAL" || error.code === "INVALID_CREDENTIAL")) return [];
2708
+ if (error instanceof LlmError && error.code === "AUTH") this.catalog.invalidate();
2709
+ this.options.onWarn?.(`claude model discovery failed; using the built-in catalog (${errorChain(error)})`);
2710
+ return this.staticModels(provider);
2711
+ }
2712
+ }
2713
+ async resolveModel(provider, model) {
2714
+ const disc = await this.discovered(model);
2374
2715
  const configured = this.options.models.find((entry) => entry.id === model);
2375
- return Promise.resolve({
2716
+ const reasoning = disc?.reasoning;
2717
+ return {
2376
2718
  provider,
2377
2719
  id: model,
2378
- name: configured?.name ?? model,
2720
+ name: disc?.name ?? configured?.name ?? model,
2379
2721
  inputModalities: configured?.inputModalities ?? CLAUDE_MODALITIES,
2380
- context: { contextWindow: configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW },
2381
- defaultMaxTokens: configured?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS
2382
- });
2722
+ context: { contextWindow: disc?.contextWindow ?? configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW },
2723
+ defaultMaxTokens: configured?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS,
2724
+ ...reasoning === void 0 ? {} : { reasoning }
2725
+ };
2383
2726
  }
2384
2727
  async *stream(options) {
2385
2728
  const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
@@ -2401,14 +2744,42 @@ var ClaudeAdapter = class extends LlmAdapter {
2401
2744
  watchdog.stop();
2402
2745
  }
2403
2746
  }
2747
+ /**
2748
+ * `display: 'summarized'` is set explicitly on both shapes: `adaptive`-type
2749
+ * models default to `display: 'omitted'`, which returns thinking blocks with
2750
+ * an empty `thinking` field — without this override the "Think" panel would
2751
+ * always render empty even though real reasoning (and billed thinking_tokens)
2752
+ * ran.
2753
+ */
2754
+ thinkingParam(thinkingType, maxTokens) {
2755
+ if (thinkingType === "adaptive") return {
2756
+ type: "adaptive",
2757
+ display: "summarized"
2758
+ };
2759
+ if (thinkingType === "enabled") {
2760
+ const budget = Math.min(Math.max(1024, Math.floor(maxTokens * .5)), maxTokens - 100);
2761
+ if (budget < 1024) return void 0;
2762
+ return {
2763
+ type: "enabled",
2764
+ budget_tokens: budget,
2765
+ display: "summarized"
2766
+ };
2767
+ }
2768
+ }
2404
2769
  async request(options, session, signal) {
2405
2770
  const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
2771
+ const maxTokens = options.maxTokens ?? this.options.models.find((entry) => entry.id === options.model)?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS;
2772
+ const disc = await this.discovered(options.model);
2773
+ const thinking = this.thinkingParam(disc?.thinkingType, maxTokens);
2774
+ const effort = options.reasoningEffort !== void 0 && disc?.reasoning !== void 0 ? { output_config: { effort: String(options.reasoningEffort) } } : {};
2406
2775
  const body = {
2407
2776
  model: options.model,
2408
- max_tokens: options.maxTokens ?? this.options.models.find((entry) => entry.id === options.model)?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS,
2777
+ max_tokens: maxTokens,
2409
2778
  system: toAnthropicSystem(options.system, messages),
2410
2779
  messages: toAnthropicMessages(messages),
2411
2780
  ...options.tools !== void 0 && options.tools.length > 0 ? { tools: toAnthropicTools(options.tools) } : {},
2781
+ ...thinking === void 0 ? {} : { thinking },
2782
+ ...effort,
2412
2783
  stream: true,
2413
2784
  ...options.sessionId !== void 0 ? { metadata: { user_id: String(options.sessionId) } } : {}
2414
2785
  };
@@ -2418,7 +2789,7 @@ var ClaudeAdapter = class extends LlmAdapter {
2418
2789
  "authorization": `Bearer ${session.accessToken}`,
2419
2790
  "anthropic-version": "2023-06-01",
2420
2791
  "anthropic-beta": CLAUDE_BETA_FLAGS,
2421
- "user-agent": CLAUDE_CLI_USER_AGENT,
2792
+ "user-agent": getClaudeCliUserAgent(),
2422
2793
  "x-app": "cli",
2423
2794
  "anthropic-dangerous-direct-browser-access": "true",
2424
2795
  "accept": "text/event-stream",
@@ -3388,19 +3759,11 @@ function createImageGenerateTool(options) {
3388
3759
  });
3389
3760
  }
3390
3761
  const revisedPrompt = images.find((image) => image.revisedPrompt !== void 0)?.revisedPrompt;
3391
- const value = {
3762
+ return {
3392
3763
  paths,
3393
3764
  ...refs.length > 0 ? { images: refs } : {},
3394
3765
  ...revisedPrompt === void 0 ? {} : { revisedPrompt }
3395
3766
  };
3396
- if (exec.parent !== void 0 && refs.length > 0) exec.deferContext(createUserMessage({
3397
- content: imageGenerateContent(value),
3398
- source: {
3399
- kind: "plugin",
3400
- plugin: "dsh-plugin-subscriptions"
3401
- }
3402
- }));
3403
- return value;
3404
3767
  }
3405
3768
  });
3406
3769
  }
@@ -3697,17 +4060,27 @@ const DEFAULT_MODELS = {
3697
4060
  ],
3698
4061
  claude: [
3699
4062
  {
3700
- id: "claude-opus-4-5",
3701
- name: "Claude Opus 4.5",
3702
- maxTokens: 64e3
4063
+ id: "claude-opus-5",
4064
+ name: "Claude Opus 5",
4065
+ maxTokens: 128e3,
4066
+ contextWindow: 1e6
4067
+ },
4068
+ {
4069
+ id: "claude-sonnet-5",
4070
+ name: "Claude Sonnet 5",
4071
+ maxTokens: 128e3,
4072
+ contextWindow: 1e6
3703
4073
  },
3704
4074
  {
3705
- id: "claude-sonnet-4-5",
3706
- name: "Claude Sonnet 4.5"
4075
+ id: "claude-fable-5",
4076
+ name: "Claude Fable 5",
4077
+ maxTokens: 128e3,
4078
+ contextWindow: 1e6
3707
4079
  },
3708
4080
  {
3709
- id: "claude-haiku-4-5",
3710
- name: "Claude Haiku 4.5"
4081
+ id: "claude-haiku-4-5-20251001",
4082
+ name: "Claude Haiku 4.5",
4083
+ maxTokens: 64e3
3711
4084
  }
3712
4085
  ],
3713
4086
  grok: [
@@ -3796,7 +4169,17 @@ var SubscriptionsAuthController = class {
3796
4169
  };
3797
4170
  }
3798
4171
  async login(provider) {
3799
- const spec = provider === "grok" ? await grokFlow() : provider === "claude" ? claudeFlow : codexFlow;
4172
+ if (provider === "claude") {
4173
+ const session = readClaudeCodeCredentials();
4174
+ if (session) {
4175
+ await this.persist("claude", session);
4176
+ this.lastError.delete("claude");
4177
+ this.onAuthChanged("claude");
4178
+ return { authorizeUrl: "" };
4179
+ }
4180
+ throw new Error("Claude Code credentials not found. Run \"claude\" first to log in.");
4181
+ }
4182
+ const spec = provider === "grok" ? await grokFlow() : codexFlow;
3800
4183
  const attempt = await this.flows.start(provider, spec);
3801
4184
  this.complete(provider, attempt);
3802
4185
  return { authorizeUrl: attempt.authorizeUrl };
@@ -3860,8 +4243,11 @@ function apply(ctx, config) {
3860
4243
  handles.get(provider)?.replace([provider]);
3861
4244
  };
3862
4245
  let codexTokens;
4246
+ let claudeTokens;
3863
4247
  let grokTokens;
3864
4248
  const usageFetchers = {};
4249
+ const speedBySession = /* @__PURE__ */ new Map();
4250
+ let codexAdapter;
3865
4251
  for (const provider of providers) switch (provider) {
3866
4252
  case "codex": {
3867
4253
  const tokens = new TokenManager({
@@ -3878,15 +4264,19 @@ function apply(ctx, config) {
3878
4264
  });
3879
4265
  codexTokens = tokens;
3880
4266
  usageFetchers.codex = async (signal) => fetchCodexUsage(await tokens.session(), fetch, signal);
3881
- handles.set("codex", ctx.llm.registerAdapter(["codex"], new CodexAdapter({
4267
+ let adapter;
4268
+ adapter = new CodexAdapter({
3882
4269
  models: catalog.codex,
3883
4270
  streamIdleTimeoutMs,
3884
4271
  tokens,
3885
4272
  discovery: !overridden.has("codex"),
3886
4273
  onWarn,
3887
4274
  resolveAttachments,
3888
- catalogStore: catalogStore("codex")
3889
- })));
4275
+ catalogStore: catalogStore("codex"),
4276
+ speedFor: (sessionId, model) => sessionId !== void 0 && speedBySession.get(sessionId) === "fast" && adapter.supportsFastTier(model)
4277
+ });
4278
+ codexAdapter = adapter;
4279
+ handles.set("codex", ctx.llm.registerAdapter(["codex"], adapter));
3890
4280
  break;
3891
4281
  }
3892
4282
  case "claude": {
@@ -3896,18 +4286,23 @@ function apply(ctx, config) {
3896
4286
  load: () => getSession("claude"),
3897
4287
  save: (session) => saveSession("claude", session),
3898
4288
  remove: () => deleteSession("claude"),
3899
- refresh: refreshClaude,
4289
+ refresh: (session) => refreshClaudeSynced(session, refreshClaude),
3900
4290
  isPermanent: isClaudePermanentRefreshError,
3901
4291
  onRemoved: () => {
3902
4292
  authChanged("claude");
3903
4293
  }
3904
4294
  });
4295
+ claudeTokens = tokens;
3905
4296
  usageFetchers.claude = async (signal) => fetchClaudeUsage(await tokens.session(), fetch, signal);
3906
4297
  handles.set("claude", ctx.llm.registerAdapter(["claude"], new ClaudeAdapter({
3907
4298
  models: catalog.claude,
3908
4299
  streamIdleTimeoutMs,
3909
4300
  tokens,
3910
- resolveAttachments
4301
+ discovery: !overridden.has("claude"),
4302
+ onWarn,
4303
+ maxRetries: 10,
4304
+ resolveAttachments,
4305
+ catalogStore: catalogStore("claude")
3911
4306
  })));
3912
4307
  break;
3913
4308
  }
@@ -3938,7 +4333,26 @@ function apply(ctx, config) {
3938
4333
  break;
3939
4334
  }
3940
4335
  }
3941
- registerAuthRpc(ctx, new SubscriptionsAuthController(flows, authChanged, resolveAttachments, usageFetchers));
4336
+ registerAuthRpc(ctx, new SubscriptionsAuthController(flows, authChanged, resolveAttachments, usageFetchers), {
4337
+ async speed(sessionId) {
4338
+ return {
4339
+ tier: speedBySession.get(sessionId) ?? "standard",
4340
+ fastModels: await codexAdapter?.fastCapableModels() ?? []
4341
+ };
4342
+ },
4343
+ async setSpeed(sessionId, tier) {
4344
+ if (tier === "standard") speedBySession.delete(sessionId);
4345
+ else speedBySession.set(sessionId, tier);
4346
+ }
4347
+ });
4348
+ if (claudeTokens !== void 0) {
4349
+ const syncTimer = setInterval(() => {
4350
+ claudeTokens?.session().catch(() => {});
4351
+ }, 5 * 6e4);
4352
+ ctx.effect(() => () => {
4353
+ clearInterval(syncTimer);
4354
+ }, "dsh-plugin-subscriptions: claude background sync timer");
4355
+ }
3942
4356
  ctx.inject(["tools"], (toolsCtx) => {
3943
4357
  if (grokTokens !== void 0) {
3944
4358
  toolsCtx.tools.register(createXSearchTool({ tokens: grokTokens }));