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/README.md
CHANGED
|
@@ -14,6 +14,10 @@ Logged-in providers join the session model picker with their live model catalogs
|
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
|
17
|
+
Models that advertise reasoning levels get an **Effort** selector in the same menu — Codex models, and Grok 4.6 / 4.5 (levels and defaults come from each provider's live catalog, not a hardcoded list):
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
17
21
|
The `image_generate` tool renders its result inline in the conversation:
|
|
18
22
|
|
|
19
23
|

|
|
@@ -24,7 +28,7 @@ The `image_generate` tool renders its result inline in the conversation:
|
|
|
24
28
|
|----------|-------------------|--------|
|
|
25
29
|
| `codex` | ChatGPT Plus/Pro | live catalog from `chatgpt.com/backend-api/codex/models` |
|
|
26
30
|
| `claude` | Claude Pro/Max | claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5 |
|
|
27
|
-
| `grok` | X Premium (xAI) | live catalog from `api.x.ai/v1/models` (chat models only) |
|
|
31
|
+
| `grok` | X Premium (xAI) | live catalog from `api.x.ai/v1/models` (chat models only); reasoning efforts from the Grok CLI catalog (`cli-chat-proxy.grok.com/v1/models`) |
|
|
28
32
|
|
|
29
33
|
Only logged-in providers appear in the session model picker; the lists above refresh on login/logout. Vision-capable models declare `['text', 'image']` input modalities, and image content is translated to each provider's wire format.
|
|
30
34
|
|
|
@@ -73,6 +77,18 @@ cp overlay.example.yml overlay.yml # then edit the name: to this checkout's ab
|
|
|
73
77
|
dsh --profile headless --patch <checkout>/overlay.yml "your task"
|
|
74
78
|
```
|
|
75
79
|
|
|
80
|
+
## Update
|
|
81
|
+
|
|
82
|
+
Installed from npm:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
dsh plugin --profile web update --latest dsh-plugin-subscriptions
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Installed from GitHub: re-run the same `add github:V1ki/dsh-plugin-subscriptions` command — it re-fetches the sources and rebuilds. A linked local checkout just needs `git pull && pnpm build` in the checkout.
|
|
89
|
+
|
|
90
|
+
Either way, restart `dsh web` afterwards so the new version loads.
|
|
91
|
+
|
|
76
92
|
## Use
|
|
77
93
|
|
|
78
94
|
1. `dsh web`, open the printed URL.
|
package/README.zh.md
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
|
17
|
+
声明了推理等级的模型会在同一菜单里多出**推理等级**选择 —— Codex 系列模型,以及 Grok 4.6 / 4.5(档位和默认值来自各 provider 的实时目录,不是硬编码列表):
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
17
21
|
`image_generate` 工具生成的图片直接内联显示在对话里:
|
|
18
22
|
|
|
19
23
|

|
|
@@ -24,7 +28,7 @@
|
|
|
24
28
|
|----------|------------------|------|
|
|
25
29
|
| `codex` | ChatGPT Plus/Pro | 从 `chatgpt.com/backend-api/codex/models` 实时获取 |
|
|
26
30
|
| `claude` | Claude Pro/Max | claude-opus-4-5、claude-sonnet-4-5、claude-haiku-4-5 |
|
|
27
|
-
| `grok` | X Premium (xAI) | 从 `api.x.ai/v1/models` 实时获取(仅对话模型) |
|
|
31
|
+
| `grok` | X Premium (xAI) | 从 `api.x.ai/v1/models` 实时获取(仅对话模型);推理等级来自 Grok CLI 目录(`cli-chat-proxy.grok.com/v1/models`) |
|
|
28
32
|
|
|
29
33
|
只有已登录的 provider 才会出现在会话模型选择器里;登录/退出后列表自动刷新。支持视觉的模型会声明 `['text', 'image']` 输入模态,图片内容会被翻译成各 provider 的 wire 格式。
|
|
30
34
|
|
|
@@ -73,6 +77,18 @@ cp overlay.example.yml overlay.yml # 然后把 name: 改成本检出的 lib/in
|
|
|
73
77
|
dsh --profile headless --patch <检出目录>/overlay.yml "你的任务"
|
|
74
78
|
```
|
|
75
79
|
|
|
80
|
+
## 更新
|
|
81
|
+
|
|
82
|
+
npm 安装的:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
dsh plugin --profile web update --latest dsh-plugin-subscriptions
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
GitHub 安装的:重新执行一遍 `add github:V1ki/dsh-plugin-subscriptions` —— 会重新拉取源码并构建。link 的本地检出只需在检出目录里 `git pull && pnpm build`。
|
|
89
|
+
|
|
90
|
+
无论哪种方式,更新后都要重启 `dsh web` 才会加载新版本。
|
|
91
|
+
|
|
76
92
|
## 使用
|
|
77
93
|
|
|
78
94
|
1. `dsh web`,打开打印的 URL。
|
package/lib/index.js
CHANGED
|
@@ -678,14 +678,26 @@ var TokenManager = class {
|
|
|
678
678
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
679
679
|
const DISCOVERY_TTL_MS = 5 * 6e4;
|
|
680
680
|
/**
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
681
|
+
* Cache for one provider's discovered model catalog. The TTL only decides
|
|
682
|
+
* when to REFRESH; it never makes the cache forget: capability metadata
|
|
683
|
+
* (reasoning efforts) must stay stable for a session that selected an effort,
|
|
684
|
+
* or mid-conversation calls fail UNSUPPORTED_REASONING_EFFORT the moment the
|
|
685
|
+
* cache goes stale. `listModels` awaits freshness via {@link get};
|
|
686
|
+
* `resolveModel` uses {@link resolve}, which serves the last-known catalog
|
|
687
|
+
* while a stale entry refreshes in the background, and only awaits the fetch
|
|
688
|
+
* when nothing is known yet. An optional {@link CatalogPersistence} seeds the
|
|
689
|
+
* last-known state across restarts and receives every successful fetch. A 401
|
|
690
|
+
* during a fetch must call {@link invalidate}.
|
|
685
691
|
*/
|
|
686
692
|
var ModelCatalogCache = class {
|
|
687
693
|
entry;
|
|
688
|
-
|
|
694
|
+
inflight;
|
|
695
|
+
/** Settles once the persisted snapshot (when any) has been considered. */
|
|
696
|
+
seeded;
|
|
697
|
+
/** Set by {@link invalidate} so an in-flight disk read cannot resurrect dropped state. */
|
|
698
|
+
seedDisabled = false;
|
|
699
|
+
constructor(persistence, ttlMs = DISCOVERY_TTL_MS) {
|
|
700
|
+
this.persistence = persistence;
|
|
689
701
|
this.ttlMs = ttlMs;
|
|
690
702
|
}
|
|
691
703
|
/**
|
|
@@ -696,27 +708,200 @@ var ModelCatalogCache = class {
|
|
|
696
708
|
if (this.entry === void 0 || Date.now() - this.entry.at >= this.ttlMs) return void 0;
|
|
697
709
|
return this.entry.models;
|
|
698
710
|
}
|
|
711
|
+
/** Load the persisted snapshot once; a fetch or invalidate that landed first wins. */
|
|
712
|
+
ensureSeeded() {
|
|
713
|
+
if (this.persistence === void 0) return Promise.resolve();
|
|
714
|
+
this.seeded ??= this.persistence.load().then((snapshot) => {
|
|
715
|
+
if (snapshot !== void 0 && this.entry === void 0 && !this.seedDisabled) this.entry = snapshot;
|
|
716
|
+
}, () => void 0);
|
|
717
|
+
return this.seeded;
|
|
718
|
+
}
|
|
719
|
+
/** Run (or join) the single in-flight fetch, updating memory and disk on success. */
|
|
720
|
+
refresh(fetcher) {
|
|
721
|
+
this.inflight ??= fetcher().then((models) => {
|
|
722
|
+
const snapshot = {
|
|
723
|
+
at: Date.now(),
|
|
724
|
+
models
|
|
725
|
+
};
|
|
726
|
+
this.entry = snapshot;
|
|
727
|
+
this.persistence?.save(snapshot).catch(() => void 0);
|
|
728
|
+
return models;
|
|
729
|
+
}).finally(() => {
|
|
730
|
+
this.inflight = void 0;
|
|
731
|
+
});
|
|
732
|
+
return this.inflight;
|
|
733
|
+
}
|
|
699
734
|
/**
|
|
700
735
|
* Return the cached catalog when fresh, otherwise fetch and cache it.
|
|
701
736
|
* @param fetcher - performs the provider's model-list request.
|
|
702
737
|
* @returns the discovered models.
|
|
738
|
+
* @throws the fetcher's failure (the `listModels` caller warns and falls back).
|
|
703
739
|
*/
|
|
704
740
|
async get(fetcher) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
741
|
+
await this.ensureSeeded();
|
|
742
|
+
return this.cached() ?? this.refresh(fetcher);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* The models for capability resolution. A fresh cache answers directly; a
|
|
746
|
+
* stale one answers immediately from the last-known catalog while a
|
|
747
|
+
* background refresh runs (a mid-conversation `resolveModel` must neither
|
|
748
|
+
* block on nor fail with the network); a cold cache awaits one fetch.
|
|
749
|
+
* @param fetcher - performs the provider's model-list request.
|
|
750
|
+
* @returns the models, or `undefined` when nothing is known (the caller
|
|
751
|
+
* falls back to its static metadata). Never throws.
|
|
752
|
+
*/
|
|
753
|
+
async resolve(fetcher) {
|
|
754
|
+
await this.ensureSeeded();
|
|
755
|
+
const fresh = this.cached();
|
|
756
|
+
if (fresh !== void 0) return fresh;
|
|
757
|
+
const known = this.entry?.models;
|
|
758
|
+
if (known !== void 0) {
|
|
759
|
+
this.refresh(fetcher).catch(() => void 0);
|
|
760
|
+
return known;
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
return await this.refresh(fetcher);
|
|
764
|
+
} catch {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
713
767
|
}
|
|
714
768
|
/** Drop the cached catalog (e.g. after a 401 proved the credential changed). */
|
|
715
769
|
invalidate() {
|
|
716
770
|
this.entry = void 0;
|
|
771
|
+
this.seedDisabled = true;
|
|
772
|
+
this.persistence?.clear().catch(() => void 0);
|
|
717
773
|
}
|
|
718
774
|
};
|
|
719
775
|
|
|
776
|
+
//#endregion
|
|
777
|
+
//#region src/providers/catalog-store.ts
|
|
778
|
+
/**
|
|
779
|
+
* Absolute path of the catalog store file.
|
|
780
|
+
* @returns `dshHomePath('plugins', 'subscriptions', 'models.json')`.
|
|
781
|
+
*/
|
|
782
|
+
function modelsFilePath() {
|
|
783
|
+
return dshHomePath("plugins", "subscriptions", "models.json");
|
|
784
|
+
}
|
|
785
|
+
/** Validate one persisted reasoning block, or undefined when malformed. */
|
|
786
|
+
function sanitizeReasoning(value) {
|
|
787
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
788
|
+
const raw = value;
|
|
789
|
+
if (!Array.isArray(raw.efforts) || raw.efforts.length === 0) return void 0;
|
|
790
|
+
const seen = /* @__PURE__ */ new Set();
|
|
791
|
+
const efforts = [];
|
|
792
|
+
for (const entry of raw.efforts) {
|
|
793
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
794
|
+
const effort = entry;
|
|
795
|
+
if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) return void 0;
|
|
796
|
+
seen.add(effort.id);
|
|
797
|
+
efforts.push({
|
|
798
|
+
id: ReasoningEffortId(effort.id),
|
|
799
|
+
name: effort.name,
|
|
800
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
if (raw.defaultEffort !== void 0 && (typeof raw.defaultEffort !== "string" || !seen.has(raw.defaultEffort))) return void 0;
|
|
804
|
+
return {
|
|
805
|
+
efforts,
|
|
806
|
+
...raw.defaultEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(raw.defaultEffort) }
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
/** Validate one persisted model, or undefined when malformed. */
|
|
810
|
+
function sanitizeModel(value) {
|
|
811
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
812
|
+
const raw = value;
|
|
813
|
+
if (typeof raw.id !== "string" || raw.id.length === 0 || typeof raw.name !== "string" || raw.name.length === 0 || raw.description !== void 0 && typeof raw.description !== "string" || raw.contextWindow !== void 0 && (typeof raw.contextWindow !== "number" || !Number.isInteger(raw.contextWindow) || raw.contextWindow <= 0) || raw.priority !== void 0 && (typeof raw.priority !== "number" || !Number.isFinite(raw.priority))) return void 0;
|
|
814
|
+
const reasoning = raw.reasoning === void 0 ? void 0 : sanitizeReasoning(raw.reasoning);
|
|
815
|
+
if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
|
|
816
|
+
return {
|
|
817
|
+
id: raw.id,
|
|
818
|
+
name: raw.name,
|
|
819
|
+
...raw.description === void 0 ? {} : { description: raw.description },
|
|
820
|
+
...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
|
|
821
|
+
...raw.priority === void 0 ? {} : { priority: raw.priority },
|
|
822
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Validate one persisted snapshot. Strict: any malformed field drops the
|
|
827
|
+
* whole snapshot rather than repairing it — the next successful discovery
|
|
828
|
+
* rewrites the entry anyway.
|
|
829
|
+
* @param value - the raw per-provider file entry.
|
|
830
|
+
* @returns the validated snapshot, or undefined when unusable.
|
|
831
|
+
*/
|
|
832
|
+
function sanitizeSnapshot(value) {
|
|
833
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
834
|
+
const raw = value;
|
|
835
|
+
if (typeof raw.at !== "number" || !Number.isFinite(raw.at)) return void 0;
|
|
836
|
+
if (!Array.isArray(raw.models) || raw.models.length === 0) return void 0;
|
|
837
|
+
const seen = /* @__PURE__ */ new Set();
|
|
838
|
+
const models = [];
|
|
839
|
+
for (const entry of raw.models) {
|
|
840
|
+
const model = sanitizeModel(entry);
|
|
841
|
+
if (model === void 0 || seen.has(model.id)) return void 0;
|
|
842
|
+
seen.add(model.id);
|
|
843
|
+
models.push(model);
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
at: raw.at,
|
|
847
|
+
models
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
/** Read the whole file; missing or unparsable reads as an empty cache. */
|
|
851
|
+
async function readCatalogFile(path) {
|
|
852
|
+
let text;
|
|
853
|
+
try {
|
|
854
|
+
text = await readFile(path, "utf8");
|
|
855
|
+
} catch {
|
|
856
|
+
return {};
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
const parsed = JSON.parse(text);
|
|
860
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
861
|
+
return parsed;
|
|
862
|
+
} catch {
|
|
863
|
+
return {};
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/** Persist the whole file atomically (tmp file + rename). */
|
|
867
|
+
async function writeCatalogFile(store, path) {
|
|
868
|
+
await mkdir(dirname(path), { recursive: true });
|
|
869
|
+
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
870
|
+
try {
|
|
871
|
+
await writeFile(tmp, JSON.stringify(store, null, 2));
|
|
872
|
+
await rename(tmp, path);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
await rm(tmp, { force: true });
|
|
875
|
+
throw error;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Build the durable half of one provider's catalog cache over the shared
|
|
880
|
+
* models.json file (concurrent writers are last-writer-wins, acceptable for
|
|
881
|
+
* a cache).
|
|
882
|
+
* @param provider - the provider route keying the file entry.
|
|
883
|
+
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
884
|
+
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
885
|
+
*/
|
|
886
|
+
function catalogStore(provider, path = modelsFilePath()) {
|
|
887
|
+
return {
|
|
888
|
+
async load() {
|
|
889
|
+
return sanitizeSnapshot((await readCatalogFile(path))[provider]);
|
|
890
|
+
},
|
|
891
|
+
async save(snapshot) {
|
|
892
|
+
const store = await readCatalogFile(path);
|
|
893
|
+
store[provider] = snapshot;
|
|
894
|
+
await writeCatalogFile(store, path);
|
|
895
|
+
},
|
|
896
|
+
async clear() {
|
|
897
|
+
const store = await readCatalogFile(path);
|
|
898
|
+
if (store[provider] === void 0) return;
|
|
899
|
+
delete store[provider];
|
|
900
|
+
await writeCatalogFile(store, path);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
720
905
|
//#endregion
|
|
721
906
|
//#region src/auth/jwt.ts
|
|
722
907
|
/** Minimal JWT payload decoding for claims extraction (no signature verification). */
|
|
@@ -1430,10 +1615,15 @@ async function fetchCodexModels(session, fetchFn = fetch) {
|
|
|
1430
1615
|
}
|
|
1431
1616
|
/** Codex wire adapter: one instance serves the `codex` provider route. */
|
|
1432
1617
|
var CodexAdapter = class extends LlmAdapter {
|
|
1433
|
-
catalog
|
|
1618
|
+
catalog;
|
|
1434
1619
|
constructor(options) {
|
|
1435
1620
|
super();
|
|
1436
1621
|
this.options = options;
|
|
1622
|
+
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
1623
|
+
}
|
|
1624
|
+
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
1625
|
+
async fetchCatalog() {
|
|
1626
|
+
return fetchCodexModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
1437
1627
|
}
|
|
1438
1628
|
providerInfo(provider) {
|
|
1439
1629
|
return {
|
|
@@ -1453,7 +1643,7 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
1453
1643
|
if (await this.options.tokens.peek() === void 0) return [];
|
|
1454
1644
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
1455
1645
|
try {
|
|
1456
|
-
return (await this.catalog.get(
|
|
1646
|
+
return (await this.catalog.get(() => this.fetchCatalog())).map((model) => ({
|
|
1457
1647
|
provider,
|
|
1458
1648
|
id: model.id,
|
|
1459
1649
|
name: model.name,
|
|
@@ -1467,10 +1657,21 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
1467
1657
|
return this.staticModels(provider);
|
|
1468
1658
|
}
|
|
1469
1659
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1660
|
+
/**
|
|
1661
|
+
* The discovered entry for one model. Resolved through the cache's
|
|
1662
|
+
* stale-while-revalidate path so capability metadata stays stable across a
|
|
1663
|
+
* long conversation: a discovered-only effort (one missing from the static
|
|
1664
|
+
* CODEX_EFFORTS list) selected by the user must not vanish — and fail the
|
|
1665
|
+
* call — just because the TTL lapsed mid-turn.
|
|
1666
|
+
*/
|
|
1667
|
+
async discovered(model) {
|
|
1668
|
+
if (!this.options.discovery) return void 0;
|
|
1669
|
+
return (await this.catalog.resolve(() => this.fetchCatalog()))?.find((entry) => entry.id === model);
|
|
1670
|
+
}
|
|
1671
|
+
async resolveModel(provider, model) {
|
|
1672
|
+
const discovered = await this.discovered(model);
|
|
1472
1673
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
1473
|
-
return
|
|
1674
|
+
return {
|
|
1474
1675
|
provider,
|
|
1475
1676
|
id: model,
|
|
1476
1677
|
name: discovered?.name ?? configured?.name ?? model,
|
|
@@ -1482,7 +1683,7 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
1482
1683
|
efforts: CODEX_EFFORTS,
|
|
1483
1684
|
defaultEffort: CODEX_DEFAULT_EFFORT
|
|
1484
1685
|
}
|
|
1485
|
-
}
|
|
1686
|
+
};
|
|
1486
1687
|
}
|
|
1487
1688
|
async *stream(options) {
|
|
1488
1689
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
@@ -2439,6 +2640,58 @@ function grokModalities(id) {
|
|
|
2439
2640
|
return /code|embed/i.test(id) ? ["text"] : ["text", "image"];
|
|
2440
2641
|
}
|
|
2441
2642
|
/**
|
|
2643
|
+
* The Grok Build CLI chat proxy's model catalog — the only grok endpoint that
|
|
2644
|
+
* advertises reasoning capability. The `api.x.ai/v1/models` and
|
|
2645
|
+
* `/v1/language-models` payloads carry pricing, context, and aliases only, so
|
|
2646
|
+
* effort metadata must come from here (the same source the official CLI's
|
|
2647
|
+
* picker uses).
|
|
2648
|
+
*/
|
|
2649
|
+
const GROK_CLI_MODELS_URL = "https://cli-chat-proxy.grok.com/v1/models";
|
|
2650
|
+
/** Map one CLI catalog entry's reasoning fields, or undefined when unsupported. */
|
|
2651
|
+
function grokCliReasoning(entry) {
|
|
2652
|
+
if (entry.supports_reasoning_effort !== true) return void 0;
|
|
2653
|
+
const efforts = (entry.reasoning_efforts ?? []).filter((level) => typeof level.value === "string" && level.value.length > 0).map((level) => ({
|
|
2654
|
+
id: ReasoningEffortId(level.value),
|
|
2655
|
+
name: typeof level.label === "string" && level.label.length > 0 ? level.label : level.value,
|
|
2656
|
+
...typeof level.description === "string" && level.description.length > 0 ? { description: level.description } : {}
|
|
2657
|
+
}));
|
|
2658
|
+
if (efforts.length === 0) return void 0;
|
|
2659
|
+
const defaultEffort = typeof entry.reasoning_effort === "string" && efforts.some((effort) => effort.id === ReasoningEffortId(entry.reasoning_effort)) ? ReasoningEffortId(entry.reasoning_effort) : void 0;
|
|
2660
|
+
return {
|
|
2661
|
+
efforts,
|
|
2662
|
+
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* Fetch the CLI catalog and index its per-model metadata by model id.
|
|
2667
|
+
* @param session - the stored session (used as-is; never refreshed here).
|
|
2668
|
+
* @param fetchFn - fetch implementation (injectable for tests).
|
|
2669
|
+
* @returns model id → contributed metadata.
|
|
2670
|
+
*/
|
|
2671
|
+
async function fetchGrokCliCatalog(session, fetchFn = fetch) {
|
|
2672
|
+
const response = await fetchFn(GROK_CLI_MODELS_URL, { headers: {
|
|
2673
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
2674
|
+
"x-xai-token-auth": "xai-grok-cli",
|
|
2675
|
+
"accept": "application/json",
|
|
2676
|
+
...attributionHeaders()
|
|
2677
|
+
} });
|
|
2678
|
+
if (!response.ok) throw await oauthEndpointError(response, "grok CLI catalog");
|
|
2679
|
+
const payload = await response.json();
|
|
2680
|
+
if (!Array.isArray(payload.data)) throw new Error("grok CLI catalog returned no data array");
|
|
2681
|
+
const catalog = /* @__PURE__ */ new Map();
|
|
2682
|
+
for (const entry of payload.data) {
|
|
2683
|
+
if (typeof entry.id !== "string" || entry.id.length === 0) continue;
|
|
2684
|
+
const reasoning = grokCliReasoning(entry);
|
|
2685
|
+
catalog.set(entry.id, {
|
|
2686
|
+
...typeof entry.name === "string" && entry.name.length > 0 ? { name: entry.name } : {},
|
|
2687
|
+
...typeof entry.description === "string" && entry.description.length > 0 ? { description: entry.description } : {},
|
|
2688
|
+
...typeof entry.context_window === "number" && entry.context_window > 0 ? { contextWindow: entry.context_window } : {},
|
|
2689
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
return catalog;
|
|
2693
|
+
}
|
|
2694
|
+
/**
|
|
2442
2695
|
* The /v1/models list also serves generation models that cannot chat
|
|
2443
2696
|
* (grok-imagine-image*, grok-imagine-video*) and embedding models; the picker
|
|
2444
2697
|
* must not offer them. Heuristic over the id substring, verified against the
|
|
@@ -2448,17 +2701,24 @@ function isChatModel(id) {
|
|
|
2448
2701
|
return !/imagine|image-|video|embed/i.test(id);
|
|
2449
2702
|
}
|
|
2450
2703
|
/**
|
|
2451
|
-
* Fetch the live grok model list
|
|
2704
|
+
* Fetch the live grok model list, enriched with the CLI catalog's per-model
|
|
2705
|
+
* metadata (display name, context window, reasoning efforts). The api.x.ai
|
|
2706
|
+
* list stays authoritative for which models exist; the CLI catalog is
|
|
2707
|
+
* enrichment only, so its failure degrades to a plain list instead of taking
|
|
2708
|
+
* discovery down — models it does not cover simply expose no efforts.
|
|
2452
2709
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
2453
2710
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
2454
|
-
* @
|
|
2711
|
+
* @param onWarn - warning sink for a failed CLI catalog fetch.
|
|
2712
|
+
* @returns discovered chat models in endpoint order.
|
|
2455
2713
|
*/
|
|
2456
|
-
async function fetchGrokModels(session, fetchFn = fetch) {
|
|
2457
|
-
const response = await fetchFn(GROK_MODELS_URL, { headers: {
|
|
2714
|
+
async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
|
|
2715
|
+
const [response, cliCatalog] = await Promise.all([fetchFn(GROK_MODELS_URL, { headers: {
|
|
2458
2716
|
"authorization": `Bearer ${session.accessToken}`,
|
|
2459
2717
|
"accept": "application/json",
|
|
2460
2718
|
...attributionHeaders()
|
|
2461
|
-
} })
|
|
2719
|
+
} }), fetchGrokCliCatalog(session, fetchFn).catch((error) => {
|
|
2720
|
+
onWarn?.(`grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})`);
|
|
2721
|
+
})]);
|
|
2462
2722
|
if (!response.ok) throw await oauthEndpointError(response, "grok models");
|
|
2463
2723
|
const payload = await response.json();
|
|
2464
2724
|
if (!Array.isArray(payload.data)) throw new Error("grok models endpoint returned no data array");
|
|
@@ -2470,7 +2730,8 @@ async function fetchGrokModels(session, fetchFn = fetch) {
|
|
|
2470
2730
|
seen.add(entry.id);
|
|
2471
2731
|
discovered.push({
|
|
2472
2732
|
id: entry.id,
|
|
2473
|
-
name: entry.id
|
|
2733
|
+
name: entry.id,
|
|
2734
|
+
...cliCatalog?.get(entry.id)
|
|
2474
2735
|
});
|
|
2475
2736
|
}
|
|
2476
2737
|
if (discovered.length === 0) throw new Error("grok models endpoint returned an empty catalog");
|
|
@@ -2478,10 +2739,15 @@ async function fetchGrokModels(session, fetchFn = fetch) {
|
|
|
2478
2739
|
}
|
|
2479
2740
|
/** Grok wire adapter: one instance serves the `grok` provider route. */
|
|
2480
2741
|
var GrokAdapter = class extends LlmAdapter {
|
|
2481
|
-
catalog
|
|
2742
|
+
catalog;
|
|
2482
2743
|
constructor(options) {
|
|
2483
2744
|
super();
|
|
2484
2745
|
this.options = options;
|
|
2746
|
+
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
2747
|
+
}
|
|
2748
|
+
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
2749
|
+
async fetchCatalog() {
|
|
2750
|
+
return fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn);
|
|
2485
2751
|
}
|
|
2486
2752
|
providerInfo(provider) {
|
|
2487
2753
|
return {
|
|
@@ -2501,10 +2767,11 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
2501
2767
|
if (await this.options.tokens.peek() === void 0) return [];
|
|
2502
2768
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
2503
2769
|
try {
|
|
2504
|
-
return (await this.catalog.get(
|
|
2770
|
+
return (await this.catalog.get(() => this.fetchCatalog())).map((model) => ({
|
|
2505
2771
|
provider,
|
|
2506
2772
|
id: model.id,
|
|
2507
2773
|
name: model.name,
|
|
2774
|
+
...model.description === void 0 ? {} : { description: model.description },
|
|
2508
2775
|
inputModalities: grokModalities(model.id)
|
|
2509
2776
|
}));
|
|
2510
2777
|
} catch (error) {
|
|
@@ -2514,17 +2781,31 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
2514
2781
|
return this.staticModels(provider);
|
|
2515
2782
|
}
|
|
2516
2783
|
}
|
|
2517
|
-
|
|
2518
|
-
|
|
2784
|
+
/**
|
|
2785
|
+
* The discovered entry for one model. Resolved through the cache's
|
|
2786
|
+
* stale-while-revalidate path: capability metadata must stay stable across
|
|
2787
|
+
* a long conversation — a session that selected a reasoning effort calls
|
|
2788
|
+
* this on EVERY step, and forgetting the efforts just because the TTL
|
|
2789
|
+
* lapsed mid-turn would fail the call with UNSUPPORTED_REASONING_EFFORT
|
|
2790
|
+
* before provider I/O.
|
|
2791
|
+
*/
|
|
2792
|
+
async discovered(model) {
|
|
2793
|
+
if (!this.options.discovery) return void 0;
|
|
2794
|
+
return (await this.catalog.resolve(() => this.fetchCatalog()))?.find((entry) => entry.id === model);
|
|
2795
|
+
}
|
|
2796
|
+
async resolveModel(provider, model) {
|
|
2797
|
+
const discovered = await this.discovered(model);
|
|
2519
2798
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
2520
|
-
return
|
|
2799
|
+
return {
|
|
2521
2800
|
provider,
|
|
2522
2801
|
id: model,
|
|
2523
2802
|
name: discovered?.name ?? configured?.name ?? model,
|
|
2803
|
+
...discovered?.description === void 0 ? {} : { description: discovered.description },
|
|
2524
2804
|
inputModalities: configured?.inputModalities ?? grokModalities(model),
|
|
2525
|
-
context: { contextWindow: configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
|
|
2526
|
-
defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS
|
|
2527
|
-
|
|
2805
|
+
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
|
|
2806
|
+
defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS,
|
|
2807
|
+
...discovered?.reasoning === void 0 ? {} : { reasoning: discovered.reasoning }
|
|
2808
|
+
};
|
|
2528
2809
|
}
|
|
2529
2810
|
async *stream(options) {
|
|
2530
2811
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
@@ -2556,6 +2837,7 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
2556
2837
|
tool_choice: "auto",
|
|
2557
2838
|
parallel_tool_calls: true,
|
|
2558
2839
|
...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
|
|
2840
|
+
...options.reasoningEffort !== void 0 ? { reasoning: { effort: String(options.reasoningEffort) } } : {},
|
|
2559
2841
|
store: false,
|
|
2560
2842
|
stream: true
|
|
2561
2843
|
};
|
|
@@ -3239,7 +3521,8 @@ function apply(ctx, config) {
|
|
|
3239
3521
|
tokens,
|
|
3240
3522
|
discovery: !overridden.has("codex"),
|
|
3241
3523
|
onWarn,
|
|
3242
|
-
resolveAttachments
|
|
3524
|
+
resolveAttachments,
|
|
3525
|
+
catalogStore: catalogStore("codex")
|
|
3243
3526
|
})));
|
|
3244
3527
|
break;
|
|
3245
3528
|
}
|
|
@@ -3286,7 +3569,8 @@ function apply(ctx, config) {
|
|
|
3286
3569
|
tokens,
|
|
3287
3570
|
discovery: !overridden.has("grok"),
|
|
3288
3571
|
onWarn,
|
|
3289
|
-
resolveAttachments
|
|
3572
|
+
resolveAttachments,
|
|
3573
|
+
catalogStore: catalogStore("grok")
|
|
3290
3574
|
})));
|
|
3291
3575
|
break;
|
|
3292
3576
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk discovered-model-catalog cache at
|
|
3
|
+
* `~/.dsh/plugins/subscriptions/models.json` — the durable half of each
|
|
4
|
+
* provider's {@link ModelCatalogCache}. One entry per provider: the last
|
|
5
|
+
* successfully discovered catalog with its fetch time, so capability metadata
|
|
6
|
+
* (reasoning efforts) survives restarts and network failures.
|
|
7
|
+
*
|
|
8
|
+
* Unlike the auth store, this file is a cache: a missing, corrupt, or
|
|
9
|
+
* malformed file silently reads as absent, because the next successful
|
|
10
|
+
* discovery rewrites it. Loads are strictly validated — a malformed entry
|
|
11
|
+
* passed through `resolveModel` would make the harness's metadata validation
|
|
12
|
+
* throw on every call, which is worse than having no fallback at all.
|
|
13
|
+
*/
|
|
14
|
+
import type { ProviderId } from '../auth/store.js';
|
|
15
|
+
import type { CatalogPersistence, CatalogSnapshot } from './common.js';
|
|
16
|
+
/**
|
|
17
|
+
* Absolute path of the catalog store file.
|
|
18
|
+
* @returns `dshHomePath('plugins', 'subscriptions', 'models.json')`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function modelsFilePath(): string;
|
|
21
|
+
/**
|
|
22
|
+
* Validate one persisted snapshot. Strict: any malformed field drops the
|
|
23
|
+
* whole snapshot rather than repairing it — the next successful discovery
|
|
24
|
+
* rewrites the entry anyway.
|
|
25
|
+
* @param value - the raw per-provider file entry.
|
|
26
|
+
* @returns the validated snapshot, or undefined when unusable.
|
|
27
|
+
*/
|
|
28
|
+
export declare function sanitizeSnapshot(value: unknown): CatalogSnapshot | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Build the durable half of one provider's catalog cache over the shared
|
|
31
|
+
* models.json file (concurrent writers are last-writer-wins, acceptable for
|
|
32
|
+
* a cache).
|
|
33
|
+
* @param provider - the provider route keying the file entry.
|
|
34
|
+
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
35
|
+
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
36
|
+
*/
|
|
37
|
+
export declare function catalogStore(provider: ProviderId, path?: string): CatalogPersistence;
|