dsh-plugin-subscriptions 0.1.2 → 0.3.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/README.md +19 -1
- package/README.zh.md +19 -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 +305 -25
- 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 +56 -0
- package/lib/providers/common.d.ts +20 -0
- package/lib/providers/grok.d.ts +51 -4
- package/lib/providers/grok.js +202 -15
- package/package.json +1 -1
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
|
|
@@ -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,14 +61,53 @@ 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
|
+
* The Grok Build CLI chat proxy's model catalog — the only grok endpoint that
|
|
84
|
+
* advertises reasoning capability. The `api.x.ai/v1/models` and
|
|
85
|
+
* `/v1/language-models` payloads carry pricing, context, and aliases only, so
|
|
86
|
+
* effort metadata must come from here (the same source the official CLI's
|
|
87
|
+
* picker uses).
|
|
88
|
+
*/
|
|
89
|
+
export declare const GROK_CLI_MODELS_URL = "https://cli-chat-proxy.grok.com/v1/models";
|
|
90
|
+
/** Per-model metadata the CLI catalog contributes to a discovered model. */
|
|
91
|
+
type GrokCliModelMeta = Partial<Pick<DiscoveredModel, 'name' | 'description' | 'contextWindow' | 'reasoning'>>;
|
|
92
|
+
/**
|
|
93
|
+
* Fetch the CLI catalog and index its per-model metadata by model id.
|
|
94
|
+
* @param session - the stored session (used as-is; never refreshed here).
|
|
95
|
+
* @param fetchFn - fetch implementation (injectable for tests).
|
|
96
|
+
* @returns model id → contributed metadata.
|
|
97
|
+
*/
|
|
98
|
+
export declare function fetchGrokCliCatalog(session: GrokSession, fetchFn?: FetchFn): Promise<Map<string, GrokCliModelMeta>>;
|
|
99
|
+
/**
|
|
100
|
+
* Fetch the live grok model list, enriched with the CLI catalog's per-model
|
|
101
|
+
* metadata (display name, context window, reasoning efforts). The api.x.ai
|
|
102
|
+
* list stays authoritative for which models exist; the CLI catalog is
|
|
103
|
+
* enrichment only, so its failure degrades to a plain list instead of taking
|
|
104
|
+
* discovery down — models it does not cover simply expose no efforts.
|
|
60
105
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
61
106
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
62
|
-
* @
|
|
107
|
+
* @param onWarn - warning sink for a failed CLI catalog fetch.
|
|
108
|
+
* @returns discovered chat models in endpoint order.
|
|
63
109
|
*/
|
|
64
|
-
export declare function fetchGrokModels(session: GrokSession, fetchFn?: FetchFn): Promise<DiscoveredModel[]>;
|
|
110
|
+
export declare function fetchGrokModels(session: GrokSession, fetchFn?: FetchFn, onWarn?: (message: string) => void): Promise<DiscoveredModel[]>;
|
|
65
111
|
/** Constructor dependencies for {@link GrokAdapter}. */
|
|
66
112
|
export interface GrokAdapterOptions {
|
|
67
113
|
models: readonly ModelEntry[];
|
|
@@ -88,3 +134,4 @@ export declare class GrokAdapter extends LlmAdapter {
|
|
|
88
134
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
89
135
|
private request;
|
|
90
136
|
}
|
|
137
|
+
export {};
|
package/lib/providers/grok.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* auth.x.ai with the Grok CLI client id, and streaming against the xAI
|
|
4
4
|
* Responses-style endpoint.
|
|
5
5
|
*/
|
|
6
|
-
import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
|
|
6
|
+
import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
7
7
|
import { decodeJwtPayload } from '../auth/jwt.js';
|
|
8
8
|
import { resolveImages } from '../translate/resolved.js';
|
|
9
9
|
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
@@ -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
|
|
@@ -181,28 +276,110 @@ function grokModalities(id) {
|
|
|
181
276
|
return /code|embed/i.test(id) ? ['text'] : ['text', 'image'];
|
|
182
277
|
}
|
|
183
278
|
/**
|
|
184
|
-
* The
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
279
|
+
* The Grok Build CLI chat proxy's model catalog — the only grok endpoint that
|
|
280
|
+
* advertises reasoning capability. The `api.x.ai/v1/models` and
|
|
281
|
+
* `/v1/language-models` payloads carry pricing, context, and aliases only, so
|
|
282
|
+
* effort metadata must come from here (the same source the official CLI's
|
|
283
|
+
* picker uses).
|
|
188
284
|
*/
|
|
189
|
-
|
|
190
|
-
|
|
285
|
+
export const GROK_CLI_MODELS_URL = 'https://cli-chat-proxy.grok.com/v1/models';
|
|
286
|
+
/** Map one CLI catalog entry's reasoning fields, or undefined when unsupported. */
|
|
287
|
+
function grokCliReasoning(entry) {
|
|
288
|
+
if (entry.supports_reasoning_effort !== true)
|
|
289
|
+
return undefined;
|
|
290
|
+
const efforts = (entry.reasoning_efforts ?? [])
|
|
291
|
+
.filter(level => typeof level.value === 'string' && level.value.length > 0)
|
|
292
|
+
.map(level => ({
|
|
293
|
+
id: ReasoningEffortId(level.value),
|
|
294
|
+
name: typeof level.label === 'string' && level.label.length > 0 ? level.label : level.value,
|
|
295
|
+
...typeof level.description === 'string' && level.description.length > 0
|
|
296
|
+
? { description: level.description }
|
|
297
|
+
: {},
|
|
298
|
+
}));
|
|
299
|
+
if (efforts.length === 0)
|
|
300
|
+
return undefined;
|
|
301
|
+
// The per-entry `default` flags are unreliable (the live catalog marks
|
|
302
|
+
// several levels default at once), so the top-level `reasoning_effort`
|
|
303
|
+
// field is the trusted default.
|
|
304
|
+
const defaultEffort = typeof entry.reasoning_effort === 'string'
|
|
305
|
+
&& efforts.some(effort => effort.id === ReasoningEffortId(entry.reasoning_effort))
|
|
306
|
+
? ReasoningEffortId(entry.reasoning_effort)
|
|
307
|
+
: undefined;
|
|
308
|
+
return { efforts, ...defaultEffort === undefined ? {} : { defaultEffort } };
|
|
191
309
|
}
|
|
192
310
|
/**
|
|
193
|
-
* Fetch the
|
|
311
|
+
* Fetch the CLI catalog and index its per-model metadata by model id.
|
|
194
312
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
195
313
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
196
|
-
* @returns
|
|
314
|
+
* @returns model id → contributed metadata.
|
|
197
315
|
*/
|
|
198
|
-
export async function
|
|
199
|
-
const response = await fetchFn(
|
|
316
|
+
export async function fetchGrokCliCatalog(session, fetchFn = fetch) {
|
|
317
|
+
const response = await fetchFn(GROK_CLI_MODELS_URL, {
|
|
200
318
|
headers: {
|
|
201
319
|
'authorization': `Bearer ${session.accessToken}`,
|
|
320
|
+
// The proxy only honors bearer tokens presented as the Grok CLI.
|
|
321
|
+
'x-xai-token-auth': 'xai-grok-cli',
|
|
202
322
|
'accept': 'application/json',
|
|
203
323
|
...attributionHeaders(),
|
|
204
324
|
},
|
|
205
325
|
});
|
|
326
|
+
if (!response.ok)
|
|
327
|
+
throw await oauthEndpointError(response, 'grok CLI catalog');
|
|
328
|
+
const payload = await response.json();
|
|
329
|
+
if (!Array.isArray(payload.data))
|
|
330
|
+
throw new Error('grok CLI catalog returned no data array');
|
|
331
|
+
const catalog = new Map();
|
|
332
|
+
for (const entry of payload.data) {
|
|
333
|
+
if (typeof entry.id !== 'string' || entry.id.length === 0)
|
|
334
|
+
continue;
|
|
335
|
+
const reasoning = grokCliReasoning(entry);
|
|
336
|
+
catalog.set(entry.id, {
|
|
337
|
+
...typeof entry.name === 'string' && entry.name.length > 0 ? { name: entry.name } : {},
|
|
338
|
+
...typeof entry.description === 'string' && entry.description.length > 0
|
|
339
|
+
? { description: entry.description }
|
|
340
|
+
: {},
|
|
341
|
+
...typeof entry.context_window === 'number' && entry.context_window > 0
|
|
342
|
+
? { contextWindow: entry.context_window }
|
|
343
|
+
: {},
|
|
344
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return catalog;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The /v1/models list also serves generation models that cannot chat
|
|
351
|
+
* (grok-imagine-image*, grok-imagine-video*) and embedding models; the picker
|
|
352
|
+
* must not offer them. Heuristic over the id substring, verified against the
|
|
353
|
+
* live catalog (grok-build-0.1 and the grok-4 family pass).
|
|
354
|
+
*/
|
|
355
|
+
function isChatModel(id) {
|
|
356
|
+
return !/imagine|image-|video|embed/i.test(id);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Fetch the live grok model list, enriched with the CLI catalog's per-model
|
|
360
|
+
* metadata (display name, context window, reasoning efforts). The api.x.ai
|
|
361
|
+
* list stays authoritative for which models exist; the CLI catalog is
|
|
362
|
+
* enrichment only, so its failure degrades to a plain list instead of taking
|
|
363
|
+
* discovery down — models it does not cover simply expose no efforts.
|
|
364
|
+
* @param session - the stored session (used as-is; never refreshed here).
|
|
365
|
+
* @param fetchFn - fetch implementation (injectable for tests).
|
|
366
|
+
* @param onWarn - warning sink for a failed CLI catalog fetch.
|
|
367
|
+
* @returns discovered chat models in endpoint order.
|
|
368
|
+
*/
|
|
369
|
+
export async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
|
|
370
|
+
const [response, cliCatalog] = await Promise.all([
|
|
371
|
+
fetchFn(GROK_MODELS_URL, {
|
|
372
|
+
headers: {
|
|
373
|
+
'authorization': `Bearer ${session.accessToken}`,
|
|
374
|
+
'accept': 'application/json',
|
|
375
|
+
...attributionHeaders(),
|
|
376
|
+
},
|
|
377
|
+
}),
|
|
378
|
+
fetchGrokCliCatalog(session, fetchFn).catch((error) => {
|
|
379
|
+
onWarn?.(`grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})`);
|
|
380
|
+
return undefined;
|
|
381
|
+
}),
|
|
382
|
+
]);
|
|
206
383
|
if (!response.ok)
|
|
207
384
|
throw await oauthEndpointError(response, 'grok models');
|
|
208
385
|
const payload = await response.json();
|
|
@@ -216,7 +393,7 @@ export async function fetchGrokModels(session, fetchFn = fetch) {
|
|
|
216
393
|
if (!isChatModel(entry.id))
|
|
217
394
|
continue;
|
|
218
395
|
seen.add(entry.id);
|
|
219
|
-
discovered.push({ id: entry.id, name: entry.id });
|
|
396
|
+
discovered.push({ id: entry.id, name: entry.id, ...cliCatalog?.get(entry.id) });
|
|
220
397
|
}
|
|
221
398
|
// An empty catalog from a 200 response is treated as a discovery failure so
|
|
222
399
|
// the adapter falls back to the static catalog instead of vanishing from
|
|
@@ -255,11 +432,12 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
255
432
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
256
433
|
// through the refresh-aware path so an expired access token renews here
|
|
257
434
|
// instead of failing discovery into the static fallback.
|
|
258
|
-
const discovered = await this.catalog.get(async () => fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn));
|
|
435
|
+
const discovered = await this.catalog.get(async () => fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn));
|
|
259
436
|
return discovered.map(model => ({
|
|
260
437
|
provider,
|
|
261
438
|
id: model.id,
|
|
262
439
|
name: model.name,
|
|
440
|
+
...model.description === undefined ? {} : { description: model.description },
|
|
263
441
|
inputModalities: grokModalities(model.id),
|
|
264
442
|
}));
|
|
265
443
|
}
|
|
@@ -284,10 +462,14 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
284
462
|
provider,
|
|
285
463
|
id: model,
|
|
286
464
|
name: discovered?.name ?? configured?.name ?? model,
|
|
465
|
+
...discovered?.description === undefined ? {} : { description: discovered.description },
|
|
287
466
|
inputModalities: configured?.inputModalities ?? grokModalities(model),
|
|
288
|
-
context: { contextWindow: configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
|
|
467
|
+
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
|
|
289
468
|
defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS,
|
|
290
|
-
//
|
|
469
|
+
// Efforts come from the discovered CLI catalog; models it does not
|
|
470
|
+
// cover expose none, so the harness rejects explicit efforts before
|
|
471
|
+
// provider I/O instead of the API 400ing.
|
|
472
|
+
...discovered?.reasoning === undefined ? {} : { reasoning: discovered.reasoning },
|
|
291
473
|
});
|
|
292
474
|
}
|
|
293
475
|
async *stream(options) {
|
|
@@ -327,6 +509,11 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
327
509
|
tool_choice: 'auto',
|
|
328
510
|
parallel_tool_calls: true,
|
|
329
511
|
...options.maxTokens !== undefined ? { max_output_tokens: options.maxTokens } : {},
|
|
512
|
+
// The harness only passes an effort the resolved model advertised (the
|
|
513
|
+
// CLI catalog's), so this never reaches a model that rejects it.
|
|
514
|
+
...options.reasoningEffort !== undefined
|
|
515
|
+
? { reasoning: { effort: String(options.reasoningEffort) } }
|
|
516
|
+
: {},
|
|
330
517
|
store: false,
|
|
331
518
|
stream: true,
|
|
332
519
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscriptions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": {
|