dsh-provider-show 0.1.0 → 0.1.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 +60 -159
- package/lib/client.js +19 -15
- package/lib/index.js +47 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,221 +3,122 @@
|
|
|
3
3
|
[](https://github.com/MosCloud/dsh-provider-show/actions/workflows/check.yml)
|
|
4
4
|
|
|
5
5
|
在 DSH 输入框右下角的「模型 · 推理强度」前面,补上**当前模型所属的供应商**。
|
|
6
|
-
|
|
7
6
|
已被其他插件接管的供应商(例如 WorkBuddy)保持原样,不重复显示。
|
|
8
7
|
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 它解决什么问题
|
|
12
|
-
|
|
13
|
-
DSH 的模型选择器(composer 模型位)自带两项信息:
|
|
14
|
-
|
|
15
|
-
| 位置 | 内容 | 由谁渲染 |
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| 模型名 | `DeepSeek-V4.1-Flash` | 宿主,经 `conversation.input.model.label` slot |
|
|
18
|
-
| 推理强度 | `Default` / `High` | 宿主,非 slot |
|
|
19
|
-
|
|
20
|
-
**缺的是第三项:这是哪个供应商的模型。**
|
|
21
|
-
|
|
22
|
-
第三方插件接管某个供应商后会自己补上这一项(WorkBuddy 画 `WB CN · 2,987`、倍率、促销角标),
|
|
23
|
-
但没有任何插件接管的供应商(例如你在设置里自建的火山方舟路由)就只有「模型 · 推理强度」,
|
|
24
|
-
看不出模型来自哪里。
|
|
25
|
-
|
|
26
|
-
本插件补齐这个缺口,并遵守一条让位规则:
|
|
27
|
-
|
|
28
|
-
> **供应商已被第三方插件接管 ⇒ 我方完全不渲染;否则 ⇒ 我方显示供应商名。**
|
|
29
|
-
|
|
30
|
-
## 效果
|
|
31
|
-
|
|
32
8
|
```
|
|
33
|
-
未接管(火山方舟) 火山方舟 DeepSeek V4.1 Flash · Default ←
|
|
34
|
-
已接管(WorkBuddy) DeepSeek-V4.1-Flash · x0.11 … ←
|
|
9
|
+
未接管(火山方舟) 火山方舟 DeepSeek V4.1 Flash · Default ← 新增「火山方舟」
|
|
10
|
+
已接管(WorkBuddy) DeepSeek-V4.1-Flash · x0.11 … ← 本插件不出现
|
|
35
11
|
```
|
|
36
12
|
|
|
37
|
-
芯片挂在模型选择器**左侧、同一条 flex 行**上,只显示供应商名:
|
|
38
|
-
模型名与推理强度宿主已经画了,重复即噪声。
|
|
39
|
-
|
|
40
13
|
## 安装
|
|
41
14
|
|
|
42
15
|
```bash
|
|
43
|
-
dsh plugin --profile <你的-profile> add
|
|
16
|
+
dsh plugin --profile <你的-profile> add dsh-provider-show
|
|
44
17
|
```
|
|
45
18
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
卸载同样通过插件管理页,或从 profile 的插件清单移除。
|
|
49
|
-
|
|
50
|
-
## 判定规则
|
|
19
|
+
也可以在插件管理页的「添加插件」里填包名或 GitHub 仓库地址。安装后**刷新页面**
|
|
20
|
+
(客户端模块是新增的,必须刷新才会加载)。
|
|
51
21
|
|
|
52
|
-
|
|
22
|
+
从源码开发时改用本地目录,改动即时生效:
|
|
53
23
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
| 2 | `settingsNs` 不在 `coreNamespaces` 内 | 判定为**已被插件接管**,让位 |
|
|
58
|
-
| 3 | 目录里查不到该 provider,或目录不可读 | 判定为**未知**,**照常显示**(fail-open) |
|
|
59
|
-
|
|
60
|
-
`settingsNs` 是「注册该 provider 的 Loader entry id」:
|
|
61
|
-
|
|
62
|
-
- `llm-pi-ai` —— 你在设置里自建的 API 路由(火山、OpenRouter、任何 OpenAI 兼容服务)
|
|
63
|
-
- `llm-deepseek` / `llm-deepseek-account` —— 官方 DeepSeek 两条路由
|
|
64
|
-
- 其他任何值 —— 某个插件用**自己的** entry id 注册的,例如 `dsh-connect-workbuddy`
|
|
24
|
+
```bash
|
|
25
|
+
dsh plugin --profile <你的-profile> add /path/to/dsh-provider-show
|
|
26
|
+
```
|
|
65
27
|
|
|
66
|
-
|
|
28
|
+
卸载通过插件管理页,或从 profile 的插件清单移除。
|
|
67
29
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
两种错误代价不对等,所以默认显示。
|
|
30
|
+
> npm 12 会留下一个 `0.0.0-stage` 占位版本(339 字节),是其暂存发布机制的产物;
|
|
31
|
+
> `latest` 指向真正的发布版本,安装不受影响。
|
|
71
32
|
|
|
72
33
|
## 配置
|
|
73
34
|
|
|
74
|
-
写在 profile 的 `cordis.patch.yml
|
|
75
|
-
|
|
35
|
+
写在 profile 的 `cordis.patch.yml` 的插件条目下。**每项都有默认值,但一处配置会替换整个
|
|
36
|
+
`config` 对象,所以只写需要改的键,其余自动取默认。**
|
|
76
37
|
|
|
77
38
|
```yaml
|
|
78
39
|
- id: dsh-provider-show
|
|
79
40
|
name: dsh-provider-show
|
|
80
41
|
config:
|
|
81
|
-
#
|
|
82
|
-
enabled: true
|
|
42
|
+
enabled: true # 总开关,默认 true
|
|
83
43
|
|
|
84
|
-
# 供应商短名,覆盖目录里的显示名
|
|
85
|
-
shortNames:
|
|
44
|
+
shortNames: # 供应商短名,覆盖目录里的显示名
|
|
86
45
|
volcano-api: 火山
|
|
87
46
|
|
|
88
|
-
#
|
|
89
|
-
overrides:
|
|
47
|
+
overrides: # 强制某个供应商的处置
|
|
90
48
|
some-connector-provider: third-party # 让位给接管它的插件
|
|
91
|
-
another-provider: mine #
|
|
92
|
-
noisy-provider: hide #
|
|
49
|
+
another-provider: mine # 即使探测说被接管也显示
|
|
50
|
+
noisy-provider: hide # 不显示,但不声称被别人接管
|
|
93
51
|
|
|
94
|
-
# 宿主自有的 settings
|
|
95
|
-
coreNamespaces:
|
|
52
|
+
coreNamespaces: # 宿主自有的 settings 命名空间,宿主升级可扩展
|
|
96
53
|
- llm-pi-ai
|
|
97
54
|
- llm-deepseek
|
|
98
55
|
- llm-deepseek-account
|
|
99
56
|
```
|
|
100
57
|
|
|
101
|
-
##
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
lib/ownership.js 纯函数:归属判定 + 文案选择(无任何 import,两侧共用)
|
|
105
|
-
lib/index.js Host 半:读 LLM 目录 → 决策 → GET /plugins/dsh-provider-show/providers
|
|
106
|
-
lib/client.js Client 半:注册 conversation.input.right,读宿主文档并渲染
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### 三个关键设计取舍
|
|
110
|
-
|
|
111
|
-
**1. 用 `conversation.input.right`,不用 `conversation.input.model.label`**
|
|
112
|
-
|
|
113
|
-
`input.model.label` 是 `single` slot:同优先级二次注册会**直接抛错**,
|
|
114
|
-
且 workbuddy 已用 `priority: -0.5` 占位、压制了宿主的 1017 徽标。
|
|
115
|
-
去抢那一格必然与第三方插件冲突。
|
|
116
|
-
|
|
117
|
-
`input.right` 是 `list` slot(`replaceRisk: "none"`),用独有 id 叠加,
|
|
118
|
-
渲染位置恰好就在模型选择器左边同一条 flex 行(`ui-conversation` 中
|
|
119
|
-
`.standardControls` 的子节点顺序即 `input.right` → `input.model`)。
|
|
120
|
-
|
|
121
|
-
代价:芯片宽度计入 `.trailing`,会让模型名**更早**折叠成图标,
|
|
122
|
-
所以芯片有 `max-width: 140px` 与省略号截断,并用 `margin-inline-end:-10px`
|
|
123
|
-
抵消宿主的 12px 栅格间距。
|
|
124
|
-
|
|
125
|
-
**排版对齐:靠"同度量",不靠位移**(由 `scripts/verify.mjs` 守住)
|
|
126
|
-
|
|
127
|
-
| 约束 | 原因 |
|
|
128
|
-
|---|---|
|
|
129
|
-
| `font-size:13px` + `font-weight:400` + `line-height:20px` + `height:28px` + `align-items:center`,且**不声明 `font-family`** | 与模型触发器 `.htuFrW_trigger` 完全同一套度量。行盒内基线位置是 `half-leading + ascent`,**由字号与字体决定、与 `line-height` 无关**;度量相同 ⇒ 基线自动重合,不需要任何对齐技巧。 |
|
|
130
|
-
| `margin-inline-end:-10px`、自身 `padding:0` | 宿主 `.standardControls` 有 12px `gap`,模型触发器又有 8px 左内边距;不补偿则两标签相距约 20px,补偿后约 10px,读作一行。 |
|
|
131
|
-
| **禁止 `translateY`** | 见下节。任何人再想加中文补偿,`verify.mjs` 会直接判红。 |
|
|
132
|
-
|
|
133
|
-
### 中文曾经"不居中"的真相(有实测数据)
|
|
134
|
-
|
|
135
|
-
早期我在 12px 字号下看到中文偏高,推断是"汉字占满 em 框、无下伸部"导致的,于是加了位移补偿。
|
|
136
|
-
**这个推断在字号不一致的前提下才成立,字号对齐后就不成立了。**
|
|
58
|
+
## 判定规则
|
|
137
59
|
|
|
138
|
-
|
|
60
|
+
「是否已被第三方插件接管」由 Host 半读取 LLM 目录后判定,优先级自上而下:
|
|
139
61
|
|
|
140
|
-
|
|
|
62
|
+
| 顺序 | 规则 | 结果 |
|
|
141
63
|
|---|---|---|
|
|
142
|
-
|
|
|
143
|
-
| `
|
|
144
|
-
|
|
|
145
|
-
| `×0.20` | 19..38 | 28.76 |
|
|
146
|
-
| `High` | 19..42 | 30.02 |
|
|
147
|
-
|
|
148
|
-
拉丁质心均值约 **29.4**。加上 `+2px` 补偿后汉字质心 33.12,比拉丁**低 3.7 图像 px ≈ 1.85 CSS px**;
|
|
149
|
-
去掉补偿正好落到 29.1,与拉丁齐平。**所以 13px 下的最优补偿是 0。**
|
|
64
|
+
| 1 | `overrides[providerId]` 显式配置 | `third-party` 让位 / `mine` 显示 / `hide` 不显示 |
|
|
65
|
+
| 2 | `settingsNs` 不在 `coreNamespaces` 内 | 判定为**已被插件接管**,让位 |
|
|
66
|
+
| 3 | 目录里查不到,或目录不可读 | 判定为**未知**,**照常显示**(fail-open) |
|
|
150
67
|
|
|
151
|
-
|
|
68
|
+
`settingsNs` 是「注册该 provider 的 Loader entry id」:
|
|
152
69
|
|
|
153
|
-
|
|
70
|
+
- `llm-pi-ai` —— 你在设置里自建的 API 路由(火山、OpenRouter、任何 OpenAI 兼容服务)
|
|
71
|
+
- `llm-deepseek` / `llm-deepseek-account` —— 官方 DeepSeek 两条路由
|
|
72
|
+
- 其他任何值 —— 某个插件用**自己的** entry id 注册的,例如 `dsh-connect-workbuddy`
|
|
154
73
|
|
|
155
|
-
|
|
156
|
-
供应商作为第三层用 `label-tertiary`、同为 13px,于是整行读作「供应商 · 模型 · 强度」一个视觉单位。
|
|
74
|
+
第 3 条选 fail-open 是因为两种错误代价不对等:漏显只是信息不全,而误判会让**所有**供应商都不显示。
|
|
157
75
|
|
|
158
|
-
|
|
76
|
+
**新会话显示什么**:会话还没有任何模型选择时(没切过模型、也没发过消息),`modelSelection`
|
|
77
|
+
投影是空的 —— 此时显示的是**部署默认模型所属的供应商**。这个默认值由 Host 从
|
|
78
|
+
`agentDefaultModel` 服务读出,与模型座位同源,所以芯片和座位不会各说各话。
|
|
79
|
+
发出第一条消息或切一次模型之后,改由该会话自己的选择决定。
|
|
80
|
+
默认通道若被第三方插件接管,仍然让位。
|
|
159
81
|
|
|
160
|
-
|
|
161
|
-
`prepareRoutes` 在进入目录前就把适配器元数据裁掉了。
|
|
162
|
-
所以规则放在 Host,客户端只做查表 —— 一处实现,不会自我漂移。
|
|
82
|
+
## 架构
|
|
163
83
|
|
|
164
|
-
|
|
84
|
+
| 文件 | 职责 |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `lib/ownership.js` | 归属判定与文案选择,纯函数、零 import,Host/Client 共用 |
|
|
87
|
+
| `lib/index.js` | Host 半:读 LLM 目录 → 决策 → 只读路由 `GET /plugins/dsh-provider-show/providers` |
|
|
88
|
+
| `lib/client.js` | Client 半:注册 `conversation.input.right`,读 Host 文档并渲染 |
|
|
165
89
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
就会让整个 Host 半加载失败。
|
|
169
|
-
因此配置用纯数据描述(`CONFIG_KEYS` / `effectiveConfig`),不导出 `Config`;
|
|
170
|
-
Loader 在没有 `Config` 时原样透传配置,默认值由本插件自己补。
|
|
90
|
+
分层原则:**浏览器看不到的运行时事实(`settingsNs`)在 Host 决策,客户端只消费结论**,
|
|
91
|
+
规则只有一份实现。设计取舍见 [docs/design.md](docs/design.md)。
|
|
171
92
|
|
|
172
93
|
## 开发
|
|
173
94
|
|
|
174
95
|
```bash
|
|
175
|
-
npm test
|
|
176
|
-
npm run check
|
|
96
|
+
npm test # 单元 + 行为测试(纯 Node,不需要 DSH 运行时)
|
|
97
|
+
npm run check # 语法检查 + 静态校验 + 全部测试
|
|
177
98
|
```
|
|
178
99
|
|
|
179
|
-
`scripts/verify.mjs`
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
路由是只读 GET 且在 `ctx.effect` 内注册、归属规则只有一份实现等。
|
|
100
|
+
`scripts/verify.mjs` 的每项检查都对应一条「否则只会在运行时才暴露」的规则:manifest 完整性、
|
|
101
|
+
图标合法性、locale 非占位文案、客户端不 import Harness Client 包、样式只用主题 token 且有宽度上界、
|
|
102
|
+
路由是只读 GET 且由 `scope.effect` 持有、归属规则只有一份实现、排版度量与宿主一致且禁止位移补偿。
|
|
183
103
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
- 需要 Host 半的只读路由(`GET /plugins/dsh-provider-show/providers`)可用;
|
|
187
|
-
该路由仅接受本页同源请求。该路径是 `/plugins/` 下的 **exact** 路由,
|
|
188
|
-
优先级高于 `/plugins` 前缀路由(webserver 先查 exact 表),因此不与客户端模块路由冲突。
|
|
189
|
-
- 芯片会让模型名更早折叠为图标(见上文取舍 1)。
|
|
190
|
-
- 当 `conversation.input.activity` 展开整行时,宿主会隐藏整组 `standardControls`,
|
|
191
|
-
芯片随之隐藏 —— 这是宿主行为。
|
|
192
|
-
- 供应商显示名默认取目录里的 `displayName`(即你在设置里填的名字);
|
|
193
|
-
想更短就配 `shortNames`。
|
|
194
|
-
- 归属判定的可靠性取决于第三方插件是否用自有 `settingsNs` 注册 provider。
|
|
195
|
-
若某插件复用核心命名空间,请用 `overrides` 显式标注。
|
|
196
|
-
|
|
197
|
-
## 状态
|
|
198
|
-
|
|
199
|
-
**已完成:安装 → 运行 → 视觉验收。全部验收项通过。**
|
|
104
|
+
零外部依赖,所以 CI 没有安装步骤——干净 checkout 直接跑同一套检查(Node 22 / 24)。
|
|
200
105
|
|
|
201
|
-
|
|
202
|
-
|---|---|
|
|
203
|
-
| 安装 | 进入 profile bundles 与依赖(本地目录 `link:`) |
|
|
204
|
-
| Host 行 | `fiberPhase: active` |
|
|
205
|
-
| 只读路由 | `GET /plugins/dsh-provider-show/providers` 实测 200,决策正确 |
|
|
206
|
-
| 真实判定 | `火山API` → 显示;`workbuddy` / `workbuddy-global` → 让位 |
|
|
207
|
-
| 视觉验收 | 供应商与模型/强度齐平、间距合适、纯拉丁与中文均正常 |
|
|
208
|
-
| 测试 | 38 项(归属规则、Host 路由行为、Client 渲染) |
|
|
209
|
-
| 静态校验 | 23 项,逐条做过反证测试 |
|
|
210
|
-
| CI | GitHub Actions 在 Node 22 / 24 上跑同一套检查 |
|
|
211
|
-
|
|
212
|
-
### 卸载
|
|
106
|
+
## 已知限制
|
|
213
107
|
|
|
214
|
-
|
|
108
|
+
- 芯片会让模型名更早折叠为图标(`max-width: 140px`,宿主按整行宽度判定折叠)。
|
|
109
|
+
- `conversation.input.activity` 展开整行时,宿主会隐藏整组 `standardControls`,芯片随之隐藏。
|
|
110
|
+
- 供应商显示名默认取目录里的 `displayName`;想更短就配 `shortNames`。
|
|
111
|
+
- 判定依赖第三方插件用自有 `settingsNs` 注册 provider;若某插件复用核心命名空间,
|
|
112
|
+
请用 `overrides` 显式标注。
|
|
113
|
+
- 只读路由仅接受本页同源请求;它是 `/plugins/` 下的 exact 路由,优先级高于客户端模块的
|
|
114
|
+
`/plugins` 前缀路由,两者不冲突。
|
|
215
115
|
|
|
216
116
|
## 文档
|
|
217
117
|
|
|
218
118
|
| 文件 | 内容 |
|
|
219
119
|
|---|---|
|
|
220
|
-
| [docs/
|
|
120
|
+
| [docs/lessons.md](docs/lessons.md) | 开发经验与踩坑记录——插槽生命周期、验证方法论、发布流程。做同类插件前先读 |
|
|
121
|
+
| [docs/design.md](docs/design.md) | 架构决策(ADR)、边界约束、验收标准、渲染与排版细节、返工留档 |
|
|
221
122
|
| [docs/research.md](docs/research.md) | 技术考据:槽位拓扑、归属判定信号、排版实测数据,均带源码文件与行号 |
|
|
222
123
|
|
|
223
124
|
## 许可
|
package/lib/client.js
CHANGED
|
@@ -144,11 +144,18 @@ window.__ModuleLoader__.load({
|
|
|
144
144
|
/**
|
|
145
145
|
* The provider label beside the model seat.
|
|
146
146
|
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
147
|
+
* Which provider is in effect is decided by a three-leg chain, strongest
|
|
148
|
+
* first: a pending selection, the last logged request header, then the
|
|
149
|
+
* deployment default. The `modelSelection` projection carries the first two
|
|
150
|
+
* (`next` is `pending ?? lastUsed`); the third exists only on the Host,
|
|
151
|
+
* because it is read from the `agentDefaultModel` service — the same one the
|
|
152
|
+
* model seat falls back to. That is why the document ships a `default`:
|
|
153
|
+
* without it a brand-new Session, which has no selection and no request yet,
|
|
154
|
+
* would show no provider at all while the seat already shows its model.
|
|
155
|
+
*
|
|
156
|
+
* Reading the seat's own authority is what keeps the two in agreement; this
|
|
157
|
+
* half still only looks a row up, and still draws nothing while the document
|
|
158
|
+
* is in flight, so a label that is about to be suppressed cannot flash.
|
|
152
159
|
*
|
|
153
160
|
* @param {object} props - slot props; `useProjection` and `t` are supplied by the slot.
|
|
154
161
|
* @returns {import('react').ReactElement | null} the label, or null.
|
|
@@ -156,14 +163,11 @@ window.__ModuleLoader__.load({
|
|
|
156
163
|
function ProviderLabel(props) {
|
|
157
164
|
const { useProjection, sessionId, t } = props
|
|
158
165
|
const selection = useProjection('modelSelection')
|
|
159
|
-
const providerId = selection?.next?.provider ?? selection?.lastUsed?.provider ?? ''
|
|
160
166
|
const [document, setDocument] = useState(undefined)
|
|
161
167
|
|
|
168
|
+
// Fetch unconditionally, including when nothing is selected: on a fresh
|
|
169
|
+
// Session the document's `default` is the ONLY source for this label.
|
|
162
170
|
useEffect(() => {
|
|
163
|
-
if (providerId === '') {
|
|
164
|
-
setDocument(undefined)
|
|
165
|
-
return undefined
|
|
166
|
-
}
|
|
167
171
|
let live = true
|
|
168
172
|
readDocument().then((next) => {
|
|
169
173
|
if (live) setDocument(next)
|
|
@@ -171,8 +175,9 @@ window.__ModuleLoader__.load({
|
|
|
171
175
|
return () => {
|
|
172
176
|
live = false
|
|
173
177
|
}
|
|
174
|
-
}, [
|
|
178
|
+
}, [sessionId])
|
|
175
179
|
|
|
180
|
+
const providerId = selection?.next?.provider ?? document?.default?.provider ?? ''
|
|
176
181
|
if (providerId === '') return null
|
|
177
182
|
const row = rowFor(document, providerId)
|
|
178
183
|
// `visible` is decided by the Host: it carries the plugin switch, the
|
|
@@ -193,15 +198,14 @@ window.__ModuleLoader__.load({
|
|
|
193
198
|
}
|
|
194
199
|
|
|
195
200
|
return {
|
|
196
|
-
// Only what the component actually needs. `remote` is deliberately absent:
|
|
197
|
-
// this half reads its own Host route, which works in every composition and
|
|
198
|
-
// keeps the ownership rule on the side that can evaluate it.
|
|
199
201
|
// Only what the component actually needs, spelled the way the cordis
|
|
200
202
|
// Loader expects (a plain array of REQUIRED service keys — the Loader has
|
|
201
203
|
// no "optional" inject form, and a key listed here parks the plugin until
|
|
202
204
|
// that service exists). `remote` is deliberately absent: this half reads
|
|
203
205
|
// its own Host route, which works in every composition and keeps the
|
|
204
|
-
// ownership rule on the side that can evaluate it.
|
|
206
|
+
// ownership rule on the side that can evaluate it. The deployment default
|
|
207
|
+
// arrives inside that document for the same reason: the browser cannot see
|
|
208
|
+
// the Host's `agentDefaultModel`.
|
|
205
209
|
inject: ['slots', 'locale'],
|
|
206
210
|
apply(ctx) {
|
|
207
211
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-provider-show: dictionaries')
|
package/lib/index.js
CHANGED
|
@@ -8,13 +8,24 @@
|
|
|
8
8
|
* small, already-decided document:
|
|
9
9
|
*
|
|
10
10
|
* GET /plugins/dsh-provider-show/providers
|
|
11
|
-
* { ok: true, value: {
|
|
11
|
+
* { ok: true, value: {
|
|
12
|
+
* providers: [{ provider, displayName, label, owner, reason }],
|
|
13
|
+
* default: { provider, model } | null,
|
|
14
|
+
* } }
|
|
12
15
|
*
|
|
13
16
|
* Each row is decided HERE by the shared pure resolver, so the ownership rule
|
|
14
17
|
* has exactly one implementation and the client half stays a lookup. No Client
|
|
15
18
|
* package, no `remote.llm` dependency, and no second copy of the rule that could
|
|
16
19
|
* drift from this one.
|
|
17
20
|
*
|
|
21
|
+
* `default` is the deployment's own default selection, read from the same
|
|
22
|
+
* `agentDefaultModel` service the composer's model seat falls back to. It is
|
|
23
|
+
* published because a brand-new Session has no model selection yet — the
|
|
24
|
+
* `modelSelection` projection stays `{ lastUsed: null, next: null }` until the
|
|
25
|
+
* first pick or the first request — and the browser has no other way to learn
|
|
26
|
+
* which provider is in effect before then. Handing over the VALUE instead of the
|
|
27
|
+
* service keeps the rule on this side: the client still only looks a row up.
|
|
28
|
+
*
|
|
18
29
|
* A Host-only bundle would need the route's consumer to exist; the Client half
|
|
19
30
|
* is what consumes it, so the two ship together in one bundle.
|
|
20
31
|
*
|
|
@@ -144,6 +155,38 @@ function read(call) {
|
|
|
144
155
|
}
|
|
145
156
|
}
|
|
146
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Read the deployment's default model selection.
|
|
160
|
+
*
|
|
161
|
+
* This is the third and last leg of the chain that decides which provider is in
|
|
162
|
+
* effect — `pending selection → last logged request header → deployment default`
|
|
163
|
+
* — and the only leg the browser cannot see. The model seat resolves it through
|
|
164
|
+
* the same service (`buildModelCatalog` defaults to
|
|
165
|
+
* `ctx.agentDefaultModel.currentSelection()`), so publishing this value is what
|
|
166
|
+
* makes the chip agree with the seat instead of guessing at it.
|
|
167
|
+
*
|
|
168
|
+
* The service is optional: a composition without `agent-default-model` has no
|
|
169
|
+
* default to publish, and a failing read is not a failing route. Both degrade to
|
|
170
|
+
* `null`, which the client renders as "draw nothing" — the failure mode this
|
|
171
|
+
* plugin has always chosen over showing a wrong label.
|
|
172
|
+
*
|
|
173
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx - host context.
|
|
174
|
+
* @returns {{ provider: string, model: string } | null} the default selection.
|
|
175
|
+
*/
|
|
176
|
+
function readDefaultSelection(ctx) {
|
|
177
|
+
try {
|
|
178
|
+
const service = ctx.get('agentDefaultModel')
|
|
179
|
+
const picked = service?.currentSelection?.()
|
|
180
|
+
if (picked === null || typeof picked !== 'object') return null
|
|
181
|
+
const { provider, model } = picked
|
|
182
|
+
if (typeof provider !== 'string' || provider === '') return null
|
|
183
|
+
if (typeof model !== 'string' || model === '') return null
|
|
184
|
+
return { provider, model }
|
|
185
|
+
} catch {
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
147
190
|
/**
|
|
148
191
|
* Build the document the client half reads.
|
|
149
192
|
*
|
|
@@ -155,8 +198,8 @@ function read(call) {
|
|
|
155
198
|
*
|
|
156
199
|
* @param {import('@deepseek-ai/cordis').Context} ctx - host context.
|
|
157
200
|
* @param {ReturnType<typeof effectiveConfig>} config - effective configuration.
|
|
158
|
-
* @returns {{ providers: Array<{provider: string, displayName: string, label: string, owner: string, reason: string}>, coreNamespaces: string[] }}
|
|
159
|
-
* one decided row per provider route.
|
|
201
|
+
* @returns {{ providers: Array<{provider: string, displayName: string, label: string, owner: string, reason: string}>, coreNamespaces: string[], default: { provider: string, model: string } | null }}
|
|
202
|
+
* one decided row per provider route, plus the deployment default.
|
|
160
203
|
*/
|
|
161
204
|
export function describeProviders(ctx, config) {
|
|
162
205
|
const { rows, providers } = readDirectory(ctx)
|
|
@@ -197,7 +240,7 @@ export function describeProviders(ctx, config) {
|
|
|
197
240
|
reason: decision.reason,
|
|
198
241
|
})
|
|
199
242
|
}
|
|
200
|
-
return { providers: out, coreNamespaces }
|
|
243
|
+
return { providers: out, coreNamespaces, default: readDefaultSelection(ctx) }
|
|
201
244
|
}
|
|
202
245
|
|
|
203
246
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-provider-show",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Shows which provider the selected model belongs to in the composer row, and stays silent for providers another plugin already presents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|