dsh-plugin-subscriptions 0.2.0 → 0.3.1
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 +17 -1
- package/README.zh.md +17 -1
- package/lib/index.js +319 -35
- package/lib/providers/catalog-store.d.ts +37 -0
- package/lib/providers/catalog-store.js +167 -0
- package/lib/providers/codex.d.ts +13 -1
- package/lib/providers/codex.js +26 -10
- package/lib/providers/common.d.ts +47 -5
- package/lib/providers/common.js +74 -11
- package/lib/providers/grok.d.ts +40 -4
- package/lib/providers/grok.js +131 -22
- package/package.json +6 -8
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';
|
|
@@ -276,28 +276,110 @@ function grokModalities(id) {
|
|
|
276
276
|
return /code|embed/i.test(id) ? ['text'] : ['text', 'image'];
|
|
277
277
|
}
|
|
278
278
|
/**
|
|
279
|
-
* The
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
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).
|
|
283
284
|
*/
|
|
284
|
-
|
|
285
|
-
|
|
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 } };
|
|
286
309
|
}
|
|
287
310
|
/**
|
|
288
|
-
* Fetch the
|
|
311
|
+
* Fetch the CLI catalog and index its per-model metadata by model id.
|
|
289
312
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
290
313
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
291
|
-
* @returns
|
|
314
|
+
* @returns model id → contributed metadata.
|
|
292
315
|
*/
|
|
293
|
-
export async function
|
|
294
|
-
const response = await fetchFn(
|
|
316
|
+
export async function fetchGrokCliCatalog(session, fetchFn = fetch) {
|
|
317
|
+
const response = await fetchFn(GROK_CLI_MODELS_URL, {
|
|
295
318
|
headers: {
|
|
296
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',
|
|
297
322
|
'accept': 'application/json',
|
|
298
323
|
...attributionHeaders(),
|
|
299
324
|
},
|
|
300
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
|
+
]);
|
|
301
383
|
if (!response.ok)
|
|
302
384
|
throw await oauthEndpointError(response, 'grok models');
|
|
303
385
|
const payload = await response.json();
|
|
@@ -311,7 +393,7 @@ export async function fetchGrokModels(session, fetchFn = fetch) {
|
|
|
311
393
|
if (!isChatModel(entry.id))
|
|
312
394
|
continue;
|
|
313
395
|
seen.add(entry.id);
|
|
314
|
-
discovered.push({ id: entry.id, name: entry.id });
|
|
396
|
+
discovered.push({ id: entry.id, name: entry.id, ...cliCatalog?.get(entry.id) });
|
|
315
397
|
}
|
|
316
398
|
// An empty catalog from a 200 response is treated as a discovery failure so
|
|
317
399
|
// the adapter falls back to the static catalog instead of vanishing from
|
|
@@ -323,10 +405,15 @@ export async function fetchGrokModels(session, fetchFn = fetch) {
|
|
|
323
405
|
/** Grok wire adapter: one instance serves the `grok` provider route. */
|
|
324
406
|
export class GrokAdapter extends LlmAdapter {
|
|
325
407
|
options;
|
|
326
|
-
catalog
|
|
408
|
+
catalog;
|
|
327
409
|
constructor(options) {
|
|
328
410
|
super();
|
|
329
411
|
this.options = options;
|
|
412
|
+
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
413
|
+
}
|
|
414
|
+
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
415
|
+
async fetchCatalog() {
|
|
416
|
+
return fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn);
|
|
330
417
|
}
|
|
331
418
|
providerInfo(provider) {
|
|
332
419
|
return { id: provider, name: 'Grok (Subscription)' };
|
|
@@ -350,11 +437,12 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
350
437
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
351
438
|
// through the refresh-aware path so an expired access token renews here
|
|
352
439
|
// instead of failing discovery into the static fallback.
|
|
353
|
-
const discovered = await this.catalog.get(
|
|
440
|
+
const discovered = await this.catalog.get(() => this.fetchCatalog());
|
|
354
441
|
return discovered.map(model => ({
|
|
355
442
|
provider,
|
|
356
443
|
id: model.id,
|
|
357
444
|
name: model.name,
|
|
445
|
+
...model.description === undefined ? {} : { description: model.description },
|
|
358
446
|
inputModalities: grokModalities(model.id),
|
|
359
447
|
}));
|
|
360
448
|
}
|
|
@@ -370,20 +458,36 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
370
458
|
return this.staticModels(provider);
|
|
371
459
|
}
|
|
372
460
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
461
|
+
/**
|
|
462
|
+
* The discovered entry for one model. Resolved through the cache's
|
|
463
|
+
* stale-while-revalidate path: capability metadata must stay stable across
|
|
464
|
+
* a long conversation — a session that selected a reasoning effort calls
|
|
465
|
+
* this on EVERY step, and forgetting the efforts just because the TTL
|
|
466
|
+
* lapsed mid-turn would fail the call with UNSUPPORTED_REASONING_EFFORT
|
|
467
|
+
* before provider I/O.
|
|
468
|
+
*/
|
|
469
|
+
async discovered(model) {
|
|
470
|
+
if (!this.options.discovery)
|
|
471
|
+
return undefined;
|
|
472
|
+
const models = await this.catalog.resolve(() => this.fetchCatalog());
|
|
473
|
+
return models?.find(entry => entry.id === model);
|
|
474
|
+
}
|
|
475
|
+
async resolveModel(provider, model) {
|
|
476
|
+
const discovered = await this.discovered(model);
|
|
377
477
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
378
|
-
return
|
|
478
|
+
return {
|
|
379
479
|
provider,
|
|
380
480
|
id: model,
|
|
381
481
|
name: discovered?.name ?? configured?.name ?? model,
|
|
482
|
+
...discovered?.description === undefined ? {} : { description: discovered.description },
|
|
382
483
|
inputModalities: configured?.inputModalities ?? grokModalities(model),
|
|
383
|
-
context: { contextWindow: configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
|
|
484
|
+
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
|
|
384
485
|
defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS,
|
|
385
|
-
//
|
|
386
|
-
|
|
486
|
+
// Efforts come from the discovered CLI catalog; models it does not
|
|
487
|
+
// cover expose none, so the harness rejects explicit efforts before
|
|
488
|
+
// provider I/O instead of the API 400ing.
|
|
489
|
+
...discovered?.reasoning === undefined ? {} : { reasoning: discovered.reasoning },
|
|
490
|
+
};
|
|
387
491
|
}
|
|
388
492
|
async *stream(options) {
|
|
389
493
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
@@ -422,6 +526,11 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
422
526
|
tool_choice: 'auto',
|
|
423
527
|
parallel_tool_calls: true,
|
|
424
528
|
...options.maxTokens !== undefined ? { max_output_tokens: options.maxTokens } : {},
|
|
529
|
+
// The harness only passes an effort the resolved model advertised (the
|
|
530
|
+
// CLI catalog's), so this never reaches a model that rejects it.
|
|
531
|
+
...options.reasoningEffort !== undefined
|
|
532
|
+
? { reasoning: { effort: String(options.reasoningEffort) } }
|
|
533
|
+
: {},
|
|
425
534
|
store: false,
|
|
426
535
|
stream: true,
|
|
427
536
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscriptions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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,12 +48,6 @@
|
|
|
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
|
-
},
|
|
57
51
|
"peerDependencies": {
|
|
58
52
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
59
53
|
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.5",
|
|
@@ -81,5 +75,9 @@
|
|
|
81
75
|
"react": "^18.2.0",
|
|
82
76
|
"tsdown": "^0.15.0",
|
|
83
77
|
"typescript": "^5.8.0"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"build": "tsc && tsdown",
|
|
81
|
+
"test": "tsc -p tsconfig.test.json && node --test lib-test/test/"
|
|
84
82
|
}
|
|
85
|
-
}
|
|
83
|
+
}
|