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.
@@ -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. */
@@ -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
- * Fetch the live grok model list.
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
- * @returns discovered chat models in endpoint order (id doubles as the name).
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 {};
@@ -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 /v1/models list also serves generation models that cannot chat
185
- * (grok-imagine-image*, grok-imagine-video*) and embedding models; the picker
186
- * must not offer them. Heuristic over the id substring, verified against the
187
- * live catalog (grok-build-0.1 and the grok-4 family pass).
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
- function isChatModel(id) {
190
- return !/imagine|image-|video|embed/i.test(id);
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 live grok model list.
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 discovered chat models in endpoint order (id doubles as the name).
314
+ * @returns model id contributed metadata.
197
315
  */
198
- export async function fetchGrokModels(session, fetchFn = fetch) {
199
- const response = await fetchFn(GROK_MODELS_URL, {
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
- // No reasoning metadata: effort selection is not exposed for grok.
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.1.2",
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": {