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.
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/README.zh.md +3 -1
- package/lib/auth/rpc.d.ts +8 -0
- package/lib/auth/rpc.js +2 -0
- package/lib/client/SubscriptionsSection.d.ts +13 -0
- package/lib/client/SubscriptionsSection.js +111 -1
- package/lib/client/locales.d.ts +20 -0
- package/lib/client/locales.js +20 -0
- package/lib/client.js +228 -0
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +238 -22
- package/lib/providers/claude.d.ts +13 -1
- package/lib/providers/claude.js +89 -0
- package/lib/providers/codex.d.ts +13 -1
- package/lib/providers/codex.js +65 -1
- package/lib/providers/common.d.ts +20 -0
- package/lib/providers/grok.d.ts +25 -1
- package/lib/providers/grok.js +104 -1
- package/package.json +8 -6
|
@@ -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[];
|
package/lib/providers/claude.js
CHANGED
|
@@ -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. */
|
package/lib/providers/codex.d.ts
CHANGED
|
@@ -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
|
package/lib/providers/codex.js
CHANGED
|
@@ -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
|
-
|
|
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. */
|
package/lib/providers/grok.d.ts
CHANGED
|
@@ -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.
|
package/lib/providers/grok.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
+
}
|