dsh-plugin-subscriptions 0.4.0 → 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
  };
@@ -3078,13 +3387,17 @@ function createXSearchTool(options) {
3078
3387
 
3079
3388
  //#endregion
3080
3389
  //#region src/tools/image-generate.ts
3081
- /** Endpoint the generation request is posted to. */
3390
+ /** Endpoint the codex generation request is posted to. */
3082
3391
  const IMAGE_GENERATE_URL = "https://chatgpt.com/backend-api/codex/images/generations";
3083
3392
  /** The image model the codex subscription endpoint serves. */
3084
3393
  const IMAGE_GENERATE_MODEL = "gpt-image-2";
3394
+ /** Endpoint the grok generation request is posted to. */
3395
+ const GROK_IMAGE_GENERATE_URL = "https://api.x.ai/v1/images/generations";
3396
+ /** The image model the grok subscription endpoint serves. */
3397
+ const GROK_IMAGE_GENERATE_MODEL = "grok-imagine-image-2.0";
3085
3398
  /**
3086
- * Assemble the request body from tool arguments (hand-checks the non-empty
3087
- * prompt the schema DSL cannot express).
3399
+ * Assemble the codex request body from tool arguments (hand-checks the
3400
+ * non-empty prompt the schema DSL cannot express).
3088
3401
  */
3089
3402
  function buildImageGenerateBody(args) {
3090
3403
  const prompt = args.prompt.trim();
@@ -3096,6 +3409,30 @@ function buildImageGenerateBody(args) {
3096
3409
  ...args.quality === void 0 ? {} : { quality: args.quality }
3097
3410
  };
3098
3411
  }
3412
+ /** The codex `size` values mapped onto grok aspect ratios. */
3413
+ const GROK_ASPECT_RATIOS = {
3414
+ "1024x1024": "1:1",
3415
+ "1024x1536": "2:3",
3416
+ "1536x1024": "3:2",
3417
+ "auto": "auto"
3418
+ };
3419
+ /**
3420
+ * Assemble the grok request body from the same tool arguments: `size` maps
3421
+ * onto the nearest `aspect_ratio`, and `quality` folds into grok's low/medium
3422
+ * pair (`high` → `medium`, `auto` → provider default).
3423
+ */
3424
+ function buildGrokImageGenerateBody(args) {
3425
+ const prompt = args.prompt.trim();
3426
+ if (prompt.length === 0) throw new Error("image_generate: prompt must be a non-empty string");
3427
+ const quality = args.quality === "low" ? "low" : args.quality === "medium" || args.quality === "high" ? "medium" : void 0;
3428
+ return {
3429
+ prompt,
3430
+ model: GROK_IMAGE_GENERATE_MODEL,
3431
+ response_format: "b64_json",
3432
+ ...args.size === void 0 ? {} : { aspect_ratio: GROK_ASPECT_RATIOS[args.size] },
3433
+ ...quality === void 0 ? {} : { quality }
3434
+ };
3435
+ }
3099
3436
  /**
3100
3437
  * Parse the generations response into decodable images. Throws when the
3101
3438
  * payload carries no usable `b64_json` entries.
@@ -3116,13 +3453,29 @@ function parseImageGenerateResponse(payload) {
3116
3453
  if (images.length === 0) throw new Error("image_generate: the response carried no image data");
3117
3454
  return images;
3118
3455
  }
3119
- /** Directory the generated PNG files are written to. */
3456
+ /** Directory the generated image files are written to. */
3120
3457
  function imagesDirectory() {
3121
3458
  return dshHomePath("plugins", "subscriptions", "images");
3122
3459
  }
3460
+ /**
3461
+ * Sniff a generated image's media type from its magic bytes (codex serves
3462
+ * PNG; grok's format is undocumented, so trust the bytes). Unrecognized data
3463
+ * defaults to PNG, matching the historical behavior.
3464
+ */
3465
+ function sniffImageMediaType(data) {
3466
+ if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
3467
+ if (data.length >= 12 && data.toString("latin1", 0, 4) === "RIFF" && data.toString("latin1", 8, 12) === "WEBP") return "image/webp";
3468
+ return "image/png";
3469
+ }
3470
+ /** File extension for one sniffed media type. */
3471
+ const MEDIA_TYPE_EXTENSIONS = {
3472
+ "image/png": "png",
3473
+ "image/jpeg": "jpg",
3474
+ "image/webp": "webp"
3475
+ };
3123
3476
  /** Timestamped, collision-safe file name for one generated image. */
3124
- function imageFileName(index) {
3125
- return `image-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}-${index}.png`;
3477
+ function imageFileName(index, mediaType) {
3478
+ return `image-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}-${index}.${MEDIA_TYPE_EXTENSIONS[mediaType]}`;
3126
3479
  }
3127
3480
  /** Bound a call-card title's prompt. */
3128
3481
  function truncate$1(text, max = 60) {
@@ -3180,7 +3533,7 @@ function imageGenerateText(value) {
3180
3533
  function createImageGenerateTool(options) {
3181
3534
  return defineTool({
3182
3535
  name: "image_generate",
3183
- description: "Generate an image with the ChatGPT subscription (gpt-image-2) and save it as a PNG file. Returns the saved file paths; on image-capable models the image itself is attached.",
3536
+ description: "Generate an image with the ChatGPT subscription (gpt-image-2) or the Grok subscription (grok-imagine-image-2.0) and save it as an image file. The `provider` parameter picks the preferred provider (default gpt); when the preferred one is logged out the other serves as fallback. Returns the saved file paths; on image-capable models the image itself is attached.",
3184
3537
  parameters: {
3185
3538
  prompt: {
3186
3539
  type: "string",
@@ -3206,6 +3559,11 @@ function createImageGenerateTool(options) {
3206
3559
  "auto"
3207
3560
  ],
3208
3561
  description: "Rendering quality; omit for the provider default."
3562
+ },
3563
+ provider: {
3564
+ type: "string",
3565
+ enum: ["gpt", "grok"],
3566
+ description: "Preferred provider (default gpt); the other one serves as fallback when the preferred is logged out."
3209
3567
  }
3210
3568
  },
3211
3569
  output: {
@@ -3268,29 +3626,57 @@ function createImageGenerateTool(options) {
3268
3626
  content: result.content.filter((block) => block.type === "text")
3269
3627
  }),
3270
3628
  async execute(args, exec) {
3271
- const body = buildImageGenerateBody(args);
3272
- const session = await options.tokens.session();
3273
- const response = await (options.fetchFn ?? fetch)(IMAGE_GENERATE_URL, {
3274
- method: "POST",
3275
- headers: {
3276
- "authorization": `Bearer ${session.accessToken}`,
3277
- "chatgpt-account-id": session.accountId,
3278
- "originator": "codex_cli_rs",
3279
- "content-type": "application/json",
3280
- "accept": "application/json"
3281
- },
3282
- body: JSON.stringify(body),
3283
- signal: exec.signal
3284
- });
3629
+ const fetchFn = options.fetchFn ?? fetch;
3630
+ const preferGrok = args.provider === "grok";
3631
+ const codexReady = options.codexTokens !== void 0 && await options.codexTokens.hasSession();
3632
+ const grokReady = options.grokTokens !== void 0 && await options.grokTokens.hasSession();
3633
+ const useGrok = preferGrok ? grokReady : grokReady && !codexReady;
3634
+ const useCodex = !useGrok && codexReady;
3635
+ let response;
3636
+ if (useCodex && options.codexTokens !== void 0) {
3637
+ const session = await options.codexTokens.session();
3638
+ response = await fetchFn(IMAGE_GENERATE_URL, {
3639
+ method: "POST",
3640
+ headers: {
3641
+ "authorization": `Bearer ${session.accessToken}`,
3642
+ "chatgpt-account-id": session.accountId,
3643
+ "originator": "codex_cli_rs",
3644
+ "content-type": "application/json",
3645
+ "accept": "application/json"
3646
+ },
3647
+ body: JSON.stringify(buildImageGenerateBody(args)),
3648
+ signal: exec.signal
3649
+ });
3650
+ } else if (useGrok && options.grokTokens !== void 0) {
3651
+ const session = await options.grokTokens.session();
3652
+ response = await fetchFn(GROK_IMAGE_GENERATE_URL, {
3653
+ method: "POST",
3654
+ headers: {
3655
+ "authorization": `Bearer ${session.accessToken}`,
3656
+ "content-type": "application/json",
3657
+ "accept": "application/json"
3658
+ },
3659
+ body: JSON.stringify(buildGrokImageGenerateBody(args)),
3660
+ signal: exec.signal
3661
+ });
3662
+ } else {
3663
+ const manager = preferGrok ? options.grokTokens ?? options.codexTokens : options.codexTokens ?? options.grokTokens;
3664
+ if (manager === void 0) throw new Error("image_generate: no image provider is configured");
3665
+ await manager.session();
3666
+ throw new Error("image_generate: no image provider is logged in");
3667
+ }
3285
3668
  if (!response.ok) throw await httpLlmError(response, "image_generate");
3286
3669
  const images = parseImageGenerateResponse(await response.json());
3287
3670
  const directory = options.imagesDir ?? imagesDirectory();
3288
3671
  await mkdir(directory, { recursive: true });
3289
3672
  const paths = [];
3673
+ const mediaTypes = [];
3290
3674
  for (const [index, image] of images.entries()) {
3291
- const path = join(directory, imageFileName(index));
3675
+ const mediaType = sniffImageMediaType(image.data);
3676
+ const path = join(directory, imageFileName(index, mediaType));
3292
3677
  await writeFile(path, image.data);
3293
3678
  paths.push(path);
3679
+ mediaTypes.push(mediaType);
3294
3680
  }
3295
3681
  const attachments = options.resolveAttachments?.();
3296
3682
  const imageCapable = attachments !== void 0 && await routeDeclaresImageInput(options.resolveLlm, exec);
@@ -3298,7 +3684,7 @@ function createImageGenerateTool(options) {
3298
3684
  if (attachments !== void 0 && imageCapable) for (const [index, image] of images.entries()) {
3299
3685
  const ref = await attachments.saveImage({
3300
3686
  data: image.data,
3301
- mediaType: "image/png",
3687
+ mediaType: mediaTypes[index],
3302
3688
  name: basename(paths[index])
3303
3689
  });
3304
3690
  refs.push({
@@ -3620,17 +4006,27 @@ const DEFAULT_MODELS = {
3620
4006
  ],
3621
4007
  claude: [
3622
4008
  {
3623
- id: "claude-opus-4-5",
3624
- name: "Claude Opus 4.5",
3625
- maxTokens: 64e3
4009
+ id: "claude-opus-5",
4010
+ name: "Claude Opus 5",
4011
+ maxTokens: 128e3,
4012
+ contextWindow: 1e6
3626
4013
  },
3627
4014
  {
3628
- id: "claude-sonnet-4-5",
3629
- name: "Claude Sonnet 4.5"
4015
+ id: "claude-sonnet-5",
4016
+ name: "Claude Sonnet 5",
4017
+ maxTokens: 128e3,
4018
+ contextWindow: 1e6
3630
4019
  },
3631
4020
  {
3632
- id: "claude-haiku-4-5",
3633
- name: "Claude Haiku 4.5"
4021
+ id: "claude-fable-5",
4022
+ name: "Claude Fable 5",
4023
+ maxTokens: 128e3,
4024
+ contextWindow: 1e6
4025
+ },
4026
+ {
4027
+ id: "claude-haiku-4-5-20251001",
4028
+ name: "Claude Haiku 4.5",
4029
+ maxTokens: 64e3
3634
4030
  }
3635
4031
  ],
3636
4032
  grok: [
@@ -3719,7 +4115,17 @@ var SubscriptionsAuthController = class {
3719
4115
  };
3720
4116
  }
3721
4117
  async login(provider) {
3722
- 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;
3723
4129
  const attempt = await this.flows.start(provider, spec);
3724
4130
  this.complete(provider, attempt);
3725
4131
  return { authorizeUrl: attempt.authorizeUrl };
@@ -3783,6 +4189,7 @@ function apply(ctx, config) {
3783
4189
  handles.get(provider)?.replace([provider]);
3784
4190
  };
3785
4191
  let codexTokens;
4192
+ let claudeTokens;
3786
4193
  let grokTokens;
3787
4194
  const usageFetchers = {};
3788
4195
  for (const provider of providers) switch (provider) {
@@ -3819,18 +4226,23 @@ function apply(ctx, config) {
3819
4226
  load: () => getSession("claude"),
3820
4227
  save: (session) => saveSession("claude", session),
3821
4228
  remove: () => deleteSession("claude"),
3822
- refresh: refreshClaude,
4229
+ refresh: (session) => refreshClaudeSynced(session, refreshClaude),
3823
4230
  isPermanent: isClaudePermanentRefreshError,
3824
4231
  onRemoved: () => {
3825
4232
  authChanged("claude");
3826
4233
  }
3827
4234
  });
4235
+ claudeTokens = tokens;
3828
4236
  usageFetchers.claude = async (signal) => fetchClaudeUsage(await tokens.session(), fetch, signal);
3829
4237
  handles.set("claude", ctx.llm.registerAdapter(["claude"], new ClaudeAdapter({
3830
4238
  models: catalog.claude,
3831
4239
  streamIdleTimeoutMs,
3832
4240
  tokens,
3833
- resolveAttachments
4241
+ discovery: !overridden.has("claude"),
4242
+ onWarn,
4243
+ maxRetries: 10,
4244
+ resolveAttachments,
4245
+ catalogStore: catalogStore("claude")
3834
4246
  })));
3835
4247
  break;
3836
4248
  }
@@ -3862,13 +4274,22 @@ function apply(ctx, config) {
3862
4274
  }
3863
4275
  }
3864
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
+ }
3865
4285
  ctx.inject(["tools"], (toolsCtx) => {
3866
4286
  if (grokTokens !== void 0) {
3867
4287
  toolsCtx.tools.register(createXSearchTool({ tokens: grokTokens }));
3868
4288
  toolsCtx.tools.register(createVideoGenerateTool({ tokens: grokTokens }));
3869
4289
  }
3870
- if (codexTokens !== void 0) toolsCtx.tools.register(createImageGenerateTool({
3871
- tokens: codexTokens,
4290
+ if (codexTokens !== void 0 || grokTokens !== void 0) toolsCtx.tools.register(createImageGenerateTool({
4291
+ ...codexTokens === void 0 ? {} : { codexTokens },
4292
+ ...grokTokens === void 0 ? {} : { grokTokens },
3872
4293
  resolveAttachments,
3873
4294
  resolveLlm: () => ctx.get("llm")
3874
4295
  }));