dsh-plugin-subscriptions 0.1.1 → 0.2.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.
@@ -9,7 +9,7 @@ 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 { ModelEntry } from './common.js';
12
+ import type { FetchFn, ModelEntry, ProviderUsage } from './common.js';
13
13
  export declare const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
14
14
  export declare const CLAUDE_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
15
15
  export declare const CLAUDE_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
@@ -40,6 +40,18 @@ export declare function refreshClaude(session: ClaudeSession): Promise<ClaudeSes
40
40
  * @returns true when re-login is the only fix.
41
41
  */
42
42
  export declare function isClaudePermanentRefreshError(error: unknown): boolean;
43
+ export declare const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
44
+ /**
45
+ * Fetch the claude subscription usage from the OAuth usage endpoint (the
46
+ * source of Claude Code's `/usage` screen). Newer responses carry a
47
+ * structured `limits` array; older ones the flat `five_hour`/`seven_day*`
48
+ * buckets — both shapes are read, the array winning when it has entries.
49
+ * @param session - the stored session (used as-is; never refreshed here).
50
+ * @param fetchFn - fetch implementation (injectable for tests).
51
+ * @param signal - caller cancellation from the RPC transport.
52
+ * @returns the mapped usage snapshot.
53
+ */
54
+ export declare function fetchClaudeUsage(session: ClaudeSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<ProviderUsage>;
43
55
  /** Constructor dependencies for {@link ClaudeAdapter}. */
44
56
  export interface ClaudeAdapterOptions {
45
57
  models: readonly ModelEntry[];
@@ -147,6 +147,95 @@ export function isClaudePermanentRefreshError(error) {
147
147
  return error instanceof OAuthEndpointError
148
148
  && (error.oauthCode === 'invalid_grant' || error.oauthCode === 'invalid_token');
149
149
  }
150
+ export const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
151
+ /** RFC3339 `resets_at` value → epoch ms, or undefined when absent/unparsable. */
152
+ function claudeResetsAt(value) {
153
+ if (typeof value !== 'string' || value.length === 0)
154
+ return undefined;
155
+ const parsed = Date.parse(value);
156
+ return Number.isFinite(parsed) ? parsed : undefined;
157
+ }
158
+ /** Map one legacy `{utilization, resets_at}` bucket; undefined when null or unusable. */
159
+ function claudeLegacyWindow(value, kind, scope) {
160
+ if (typeof value !== 'object' || value === null)
161
+ return undefined;
162
+ const bucket = value;
163
+ if (typeof bucket.utilization !== 'number' || !Number.isFinite(bucket.utilization))
164
+ return undefined;
165
+ const resetsAt = claudeResetsAt(bucket.resets_at);
166
+ return {
167
+ kind,
168
+ ...scope === undefined ? {} : { scope },
169
+ usedPercent: bucket.utilization,
170
+ ...resetsAt === undefined ? {} : { resetsAt },
171
+ };
172
+ }
173
+ /** Map the modern `limits` array; empty when absent or carrying nothing usable. */
174
+ function claudeLimitsWindows(value) {
175
+ if (!Array.isArray(value))
176
+ return [];
177
+ const windows = [];
178
+ for (const raw of value) {
179
+ if (typeof raw !== 'object' || raw === null)
180
+ continue;
181
+ const entry = raw;
182
+ if (typeof entry.percent !== 'number' || !Number.isFinite(entry.percent))
183
+ continue;
184
+ const kind = entry.kind === 'session'
185
+ ? 'session'
186
+ : entry.kind === 'weekly_all' || entry.kind === 'weekly_scoped' ? 'weekly' : 'other';
187
+ const scope = entry.scope?.model?.display_name;
188
+ const resetsAt = claudeResetsAt(entry.resets_at);
189
+ windows.push({
190
+ kind,
191
+ ...typeof scope === 'string' && scope.length > 0 ? { scope } : {},
192
+ usedPercent: entry.percent,
193
+ ...resetsAt === undefined ? {} : { resetsAt },
194
+ });
195
+ }
196
+ return windows;
197
+ }
198
+ /**
199
+ * Fetch the claude subscription usage from the OAuth usage endpoint (the
200
+ * source of Claude Code's `/usage` screen). Newer responses carry a
201
+ * structured `limits` array; older ones the flat `five_hour`/`seven_day*`
202
+ * buckets — both shapes are read, the array winning when it has entries.
203
+ * @param session - the stored session (used as-is; never refreshed here).
204
+ * @param fetchFn - fetch implementation (injectable for tests).
205
+ * @param signal - caller cancellation from the RPC transport.
206
+ * @returns the mapped usage snapshot.
207
+ */
208
+ export async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
209
+ const response = await fetchFn(CLAUDE_USAGE_URL, {
210
+ headers: {
211
+ 'authorization': `Bearer ${session.accessToken}`,
212
+ 'anthropic-beta': 'oauth-2025-04-20',
213
+ // Unrecognized clients are aggressively rate-limited on this endpoint,
214
+ // so it presents as the CLI like every other subscription request.
215
+ 'user-agent': CLAUDE_CLI_USER_AGENT,
216
+ 'accept': 'application/json',
217
+ },
218
+ ...signal === undefined ? {} : { signal },
219
+ });
220
+ if (!response.ok)
221
+ throw await oauthEndpointError(response, 'claude usage');
222
+ const payload = await response.json();
223
+ const modern = claudeLimitsWindows(payload.limits);
224
+ if (modern.length > 0)
225
+ return { supported: true, windows: modern };
226
+ const windows = [];
227
+ const legacy = [
228
+ claudeLegacyWindow(payload.five_hour, 'session'),
229
+ claudeLegacyWindow(payload.seven_day, 'weekly'),
230
+ claudeLegacyWindow(payload.seven_day_opus, 'weekly', 'Opus'),
231
+ claudeLegacyWindow(payload.seven_day_sonnet, 'weekly', 'Sonnet'),
232
+ ];
233
+ for (const window of legacy) {
234
+ if (window !== undefined)
235
+ windows.push(window);
236
+ }
237
+ return { supported: true, windows };
238
+ }
150
239
  /** The Claude 4.5 family accepts image input. */
151
240
  const CLAUDE_MODALITIES = ['text', 'image'];
152
241
  /** Claude wire adapter: one instance serves the `claude` provider route. */
@@ -9,7 +9,7 @@ import type { FlowSpec } from '../auth/oauth-flow.js';
9
9
  import type { CodexSession } from '../auth/store.js';
10
10
  import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
11
11
  import { TokenManager } from './common.js';
12
- import type { DiscoveredModel, FetchFn, ModelEntry } from './common.js';
12
+ import type { DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
13
13
  export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
14
14
  export declare const CODEX_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
15
15
  export declare const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
@@ -53,6 +53,18 @@ export declare function refreshCodex(session: CodexSession): Promise<CodexSessio
53
53
  * @returns true when re-login is the only fix.
54
54
  */
55
55
  export declare function isCodexPermanentRefreshError(error: unknown): boolean;
56
+ export declare const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
57
+ /**
58
+ * Fetch the codex subscription usage from the ChatGPT backend wham/usage
59
+ * endpoint (the source of the codex CLI `/status` rate-limit lines). The
60
+ * primary window is the rolling session (5-hour) lane, the secondary window
61
+ * the weekly lane; the lookup itself consumes no rate-limit budget.
62
+ * @param session - the stored session (used as-is; never refreshed here).
63
+ * @param fetchFn - fetch implementation (injectable for tests).
64
+ * @param signal - caller cancellation from the RPC transport.
65
+ * @returns the mapped usage snapshot.
66
+ */
67
+ export declare function fetchCodexUsage(session: CodexSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<ProviderUsage>;
56
68
  export declare const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
57
69
  /**
58
70
  * Client version sent on the /models catalog request. The backend gates the
@@ -188,6 +188,62 @@ export function isCodexPermanentRefreshError(error) {
188
188
  && error.oauthCode !== undefined
189
189
  && PERMANENT_REFRESH_CODES.has(error.oauthCode);
190
190
  }
191
+ export const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
192
+ /** Map one wham/usage window into a {@link UsageWindow}; undefined when unusable. */
193
+ function codexUsageWindow(value, kind) {
194
+ if (typeof value !== 'object' || value === null)
195
+ return undefined;
196
+ const window = value;
197
+ if (typeof window.used_percent !== 'number' || !Number.isFinite(window.used_percent))
198
+ return undefined;
199
+ let resetsAt;
200
+ if (typeof window.reset_at === 'number' && window.reset_at > 0) {
201
+ resetsAt = window.reset_at * 1000;
202
+ }
203
+ else if (typeof window.reset_after_seconds === 'number' && window.reset_after_seconds > 0) {
204
+ resetsAt = Date.now() + window.reset_after_seconds * 1000;
205
+ }
206
+ return { kind, usedPercent: window.used_percent, ...resetsAt === undefined ? {} : { resetsAt } };
207
+ }
208
+ /**
209
+ * Fetch the codex subscription usage from the ChatGPT backend wham/usage
210
+ * endpoint (the source of the codex CLI `/status` rate-limit lines). The
211
+ * primary window is the rolling session (5-hour) lane, the secondary window
212
+ * the weekly lane; the lookup itself consumes no rate-limit budget.
213
+ * @param session - the stored session (used as-is; never refreshed here).
214
+ * @param fetchFn - fetch implementation (injectable for tests).
215
+ * @param signal - caller cancellation from the RPC transport.
216
+ * @returns the mapped usage snapshot.
217
+ */
218
+ export async function fetchCodexUsage(session, fetchFn = fetch, signal) {
219
+ const response = await fetchFn(CODEX_USAGE_URL, {
220
+ headers: {
221
+ 'authorization': `Bearer ${session.accessToken}`,
222
+ 'chatgpt-account-id': session.accountId,
223
+ 'originator': 'codex_cli_rs',
224
+ 'accept': 'application/json',
225
+ ...attributionHeaders(),
226
+ },
227
+ ...signal === undefined ? {} : { signal },
228
+ });
229
+ if (!response.ok)
230
+ throw await oauthEndpointError(response, 'codex usage');
231
+ const payload = await response.json();
232
+ const windows = [];
233
+ const primary = codexUsageWindow(payload.rate_limit?.primary_window, 'session');
234
+ const secondary = codexUsageWindow(payload.rate_limit?.secondary_window, 'weekly');
235
+ if (primary !== undefined)
236
+ windows.push(primary);
237
+ if (secondary !== undefined)
238
+ windows.push(secondary);
239
+ return {
240
+ supported: true,
241
+ windows,
242
+ ...typeof payload.plan_type === 'string' && payload.plan_type.length > 0
243
+ ? { plan: payload.plan_type }
244
+ : {},
245
+ };
246
+ }
191
247
  export const CODEX_MODELS_URL = 'https://chatgpt.com/backend-api/codex/models';
192
248
  /**
193
249
  * Client version sent on the /models catalog request. The backend gates the
@@ -295,7 +351,10 @@ export class CodexAdapter extends LlmAdapter {
295
351
  if (!this.options.discovery)
296
352
  return this.staticModels(provider);
297
353
  try {
298
- const discovered = await this.catalog.get(() => fetchCodexModels(session, this.options.fetchFn));
354
+ // The fetcher runs only on a cache miss, and resolves the session
355
+ // through the refresh-aware path so an expired access token renews here
356
+ // instead of failing discovery into the static fallback.
357
+ const discovered = await this.catalog.get(async () => fetchCodexModels(await this.options.tokens.session(), this.options.fetchFn));
299
358
  return discovered.map(model => ({
300
359
  provider,
301
360
  id: model.id,
@@ -305,6 +364,11 @@ export class CodexAdapter extends LlmAdapter {
305
364
  }));
306
365
  }
307
366
  catch (error) {
367
+ // A permanent refresh failure deletes the stored session: the provider
368
+ // is logged out, so hide it instead of showing a stale static catalog.
369
+ if (error instanceof LlmError
370
+ && (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL'))
371
+ return [];
308
372
  if (error instanceof OAuthEndpointError && error.status === 401)
309
373
  this.catalog.invalidate();
310
374
  this.options.onWarn?.(`codex model discovery failed; using the built-in catalog (${errorChain(error)})`);
@@ -135,6 +135,26 @@ export declare class TokenManager<S extends TimedSession> {
135
135
  }
136
136
  /** Fetch signature adapters accept for discovery calls (injectable for tests). */
137
137
  export type FetchFn = typeof fetch;
138
+ /** One rate-limit window reported by a provider's usage endpoint. */
139
+ export interface UsageWindow {
140
+ /** Window kind: `session` for the short rolling window, `weekly` for the 7-day one. */
141
+ kind: 'session' | 'weekly' | 'other';
142
+ /** Model scope for model-specific windows (e.g. `Opus`), when the provider names one. */
143
+ scope?: string;
144
+ /** Percent of the window already consumed (0–100). */
145
+ usedPercent: number;
146
+ /** Epoch milliseconds at which the window resets, when the provider discloses it. */
147
+ resetsAt?: number;
148
+ }
149
+ /** Subscription usage of one provider, as served by the `usage` RPC endpoint. */
150
+ export interface ProviderUsage {
151
+ /** False when the provider has no usage endpoint (grok); windows are absent then. */
152
+ supported: boolean;
153
+ /** Usage windows in display order. */
154
+ windows?: UsageWindow[];
155
+ /** Plan name the usage endpoint reported, when present. */
156
+ plan?: string;
157
+ }
138
158
  /** One model discovered from a provider's live model-list endpoint. */
139
159
  export interface DiscoveredModel {
140
160
  /** Wire model id. */
@@ -9,7 +9,7 @@ import type { FlowSpec } from '../auth/oauth-flow.js';
9
9
  import type { GrokSession } from '../auth/store.js';
10
10
  import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
11
11
  import { TokenManager } from './common.js';
12
- import type { DiscoveredModel, FetchFn, ModelEntry } from './common.js';
12
+ import type { DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
13
13
  export declare const GROK_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
14
14
  export declare const GROK_DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration";
15
15
  export declare const GROK_API_URL = "https://api.x.ai/v1/responses";
@@ -31,6 +31,13 @@ export declare function grokDiscovery(): Promise<GrokDiscovery>;
31
31
  * @returns the flow spec for one attempt.
32
32
  */
33
33
  export declare function grokFlow(): Promise<FlowSpec>;
34
+ /**
35
+ * The subscription tier encoded in a grok access token's `tier` claim (no
36
+ * verification — same trust posture as the other claim reads).
37
+ * @param accessToken - the stored access token.
38
+ * @returns the display tier name, or undefined when the claim is absent.
39
+ */
40
+ export declare function grokTierName(accessToken: string): string | undefined;
34
41
  /**
35
42
  * Exchange an authorization code for a grok session (form-encoded grant that
36
43
  * echoes the PKCE challenge as well as the verifier, per the xAI flow).
@@ -54,6 +61,23 @@ export declare function refreshGrok(session: GrokSession): Promise<GrokSession>;
54
61
  * @returns true when re-login is the only fix.
55
62
  */
56
63
  export declare function isGrokPermanentRefreshError(error: unknown): boolean;
64
+ /**
65
+ * The Grok Build CLI chat proxy's billing endpoint (the source of the CLI's
66
+ * `/usage` "Usage limit" panel; see xai-org/grok-build
67
+ * `extensions/billing.rs`). Forwards to the backend `GetGrokCreditsConfig`.
68
+ */
69
+ export declare const GROK_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
70
+ /**
71
+ * Fetch the grok subscription usage from the Grok Build CLI chat proxy. The
72
+ * newer credits config carries a ready-made percentage plus the current
73
+ * (typically weekly) period; the legacy shape carries cent-valued
74
+ * `monthlyLimit`/`used`, from which the percentage is derived.
75
+ * @param session - the stored session (used as-is; never refreshed here).
76
+ * @param fetchFn - fetch implementation (injectable for tests).
77
+ * @param signal - caller cancellation from the RPC transport.
78
+ * @returns the mapped usage snapshot.
79
+ */
80
+ export declare function fetchGrokUsage(session: GrokSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<ProviderUsage>;
57
81
  export declare const GROK_MODELS_URL = "https://api.x.ai/v1/models";
58
82
  /**
59
83
  * Fetch the live grok model list.
@@ -80,6 +80,34 @@ export async function grokFlow() {
80
80
  },
81
81
  };
82
82
  }
83
+ /**
84
+ * Display names for the numeric `tier` claim xAI stamps on OAuth access
85
+ * tokens (the `prod_auth.SubscriptionTier` proto enum; the mapping mirrors
86
+ * grok-build's `jwt_tier_claim`). Unknown values fall through to the raw
87
+ * number so a future tier still shows something.
88
+ */
89
+ const GROK_TIER_NAMES = {
90
+ 0: 'Free',
91
+ 1: 'SuperGrok',
92
+ 2: 'X Basic',
93
+ 3: 'X Premium',
94
+ 4: 'X Premium+',
95
+ 5: 'SuperGrok Heavy',
96
+ 6: 'SuperGrok Lite',
97
+ 7: 'SuperGrok Plus',
98
+ };
99
+ /**
100
+ * The subscription tier encoded in a grok access token's `tier` claim (no
101
+ * verification — same trust posture as the other claim reads).
102
+ * @param accessToken - the stored access token.
103
+ * @returns the display tier name, or undefined when the claim is absent.
104
+ */
105
+ export function grokTierName(accessToken) {
106
+ const tier = decodeJwtPayload(accessToken)?.tier;
107
+ if (typeof tier !== 'number' || !Number.isInteger(tier))
108
+ return undefined;
109
+ return GROK_TIER_NAMES[tier] ?? String(tier);
110
+ }
83
111
  /** Pick a display account from an id token's claims. */
84
112
  function grokAccount(idToken) {
85
113
  const payload = idToken === undefined ? undefined : decodeJwtPayload(idToken);
@@ -172,6 +200,73 @@ export async function refreshGrok(session) {
172
200
  export function isGrokPermanentRefreshError(error) {
173
201
  return error instanceof OAuthEndpointError && error.oauthCode === 'invalid_grant';
174
202
  }
203
+ /**
204
+ * The Grok Build CLI chat proxy's billing endpoint (the source of the CLI's
205
+ * `/usage` "Usage limit" panel; see xai-org/grok-build
206
+ * `extensions/billing.rs`). Forwards to the backend `GetGrokCreditsConfig`.
207
+ */
208
+ export const GROK_BILLING_URL = 'https://cli-chat-proxy.grok.com/v1/billing?format=credits';
209
+ /** RFC3339 timestamp → epoch ms, or undefined when absent/unparsable. */
210
+ function grokResetsAt(value) {
211
+ if (typeof value !== 'string' || value.length === 0)
212
+ return undefined;
213
+ const parsed = Date.parse(value);
214
+ return Number.isFinite(parsed) ? parsed : undefined;
215
+ }
216
+ /**
217
+ * Fetch the grok subscription usage from the Grok Build CLI chat proxy. The
218
+ * newer credits config carries a ready-made percentage plus the current
219
+ * (typically weekly) period; the legacy shape carries cent-valued
220
+ * `monthlyLimit`/`used`, from which the percentage is derived.
221
+ * @param session - the stored session (used as-is; never refreshed here).
222
+ * @param fetchFn - fetch implementation (injectable for tests).
223
+ * @param signal - caller cancellation from the RPC transport.
224
+ * @returns the mapped usage snapshot.
225
+ */
226
+ export async function fetchGrokUsage(session, fetchFn = fetch, signal) {
227
+ const response = await fetchFn(GROK_BILLING_URL, {
228
+ headers: {
229
+ 'authorization': `Bearer ${session.accessToken}`,
230
+ // The proxy only honors bearer tokens presented as the Grok CLI.
231
+ 'x-xai-token-auth': 'xai-grok-cli',
232
+ 'accept': 'application/json',
233
+ ...attributionHeaders(),
234
+ },
235
+ ...signal === undefined ? {} : { signal },
236
+ });
237
+ if (!response.ok)
238
+ throw await oauthEndpointError(response, 'grok billing');
239
+ const payload = await response.json();
240
+ const config = typeof payload.config === 'object' && payload.config !== null ? payload.config : {};
241
+ const windows = [];
242
+ if (typeof config.creditUsagePercent === 'number' && Number.isFinite(config.creditUsagePercent)) {
243
+ const kind = config.currentPeriod?.type === 'USAGE_PERIOD_TYPE_WEEKLY'
244
+ ? 'weekly'
245
+ : 'other';
246
+ const resetsAt = grokResetsAt(config.currentPeriod?.end);
247
+ windows.push({ kind, usedPercent: config.creditUsagePercent, ...resetsAt === undefined ? {} : { resetsAt } });
248
+ }
249
+ else if (typeof config.monthlyLimit?.val === 'number' && config.monthlyLimit.val > 0) {
250
+ const used = typeof config.used?.val === 'number' ? config.used.val : 0;
251
+ const resetsAt = grokResetsAt(config.billingPeriodEnd);
252
+ windows.push({
253
+ kind: 'other',
254
+ usedPercent: (used / config.monthlyLimit.val) * 100,
255
+ ...resetsAt === undefined ? {} : { resetsAt },
256
+ });
257
+ }
258
+ // The upstream billing response rarely carries `subscriptionTier` (the CLI
259
+ // enriches it locally from its settings cache), so the access token's
260
+ // `tier` claim is the working fallback.
261
+ const plan = typeof payload.subscriptionTier === 'string' && payload.subscriptionTier.length > 0
262
+ ? payload.subscriptionTier
263
+ : grokTierName(session.accessToken);
264
+ return {
265
+ supported: true,
266
+ windows,
267
+ ...plan === undefined ? {} : { plan },
268
+ };
269
+ }
175
270
  export const GROK_MODELS_URL = 'https://api.x.ai/v1/models';
176
271
  /**
177
272
  * Input modalities for one grok model: chat models (grok-4 family) accept
@@ -252,7 +347,10 @@ export class GrokAdapter extends LlmAdapter {
252
347
  if (!this.options.discovery)
253
348
  return this.staticModels(provider);
254
349
  try {
255
- const discovered = await this.catalog.get(() => fetchGrokModels(session, this.options.fetchFn));
350
+ // The fetcher runs only on a cache miss, and resolves the session
351
+ // through the refresh-aware path so an expired access token renews here
352
+ // instead of failing discovery into the static fallback.
353
+ const discovered = await this.catalog.get(async () => fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn));
256
354
  return discovered.map(model => ({
257
355
  provider,
258
356
  id: model.id,
@@ -261,6 +359,11 @@ export class GrokAdapter extends LlmAdapter {
261
359
  }));
262
360
  }
263
361
  catch (error) {
362
+ // A permanent refresh failure deletes the stored session: the provider
363
+ // is logged out, so hide it instead of showing a stale static catalog.
364
+ if (error instanceof LlmError
365
+ && (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL'))
366
+ return [];
264
367
  if (error instanceof OAuthEndpointError && error.status === 401)
265
368
  this.catalog.invalidate();
266
369
  this.options.onWarn?.(`grok model discovery failed; using the built-in catalog (${errorChain(error)})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subscriptions",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Use ChatGPT (Codex), Claude, and Grok (X Premium) subscriptions as DeepSeek Harness LLM providers, with OAuth login from the web Settings page",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,6 +48,12 @@
48
48
  ]
49
49
  }
50
50
  },
51
+ "scripts": {
52
+ "build": "tsc && tsdown",
53
+ "test": "tsc -p tsconfig.test.json && node --test lib-test/test/",
54
+ "prepare": "tsdown -c tsdown.prepare.config.ts",
55
+ "prepublishOnly": "pnpm build && pnpm test"
56
+ },
51
57
  "peerDependencies": {
52
58
  "@deepseek-ai/cordis": "^4.0.1",
53
59
  "@deepseek-ai/dsh-attachment": "^0.1.0-rc.5",
@@ -75,9 +81,5 @@
75
81
  "react": "^18.2.0",
76
82
  "tsdown": "^0.15.0",
77
83
  "typescript": "^5.8.0"
78
- },
79
- "scripts": {
80
- "build": "tsc && tsdown",
81
- "test": "tsc -p tsconfig.test.json && node --test lib-test/test/"
82
84
  }
83
- }
85
+ }