@leaves615/dsh-llm-ctl 0.1.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/LICENSE +21 -0
- package/README.md +174 -0
- package/cordis.patch.yml +7 -0
- package/lib/client-plugin.d.ts +82 -0
- package/lib/client-plugin.js +685 -0
- package/lib/client.js +1712 -0
- package/lib/concurrency.d.ts +28 -0
- package/lib/concurrency.js +36 -0
- package/lib/config.d.ts +203 -0
- package/lib/config.js +67 -0
- package/lib/controller.d.ts +42 -0
- package/lib/controller.js +51 -0
- package/lib/delay.d.ts +68 -0
- package/lib/delay.js +134 -0
- package/lib/discover-ui.d.ts +94 -0
- package/lib/discover-ui.js +91 -0
- package/lib/discover.d.ts +79 -0
- package/lib/discover.js +141 -0
- package/lib/events.d.ts +45 -0
- package/lib/events.js +37 -0
- package/lib/index.d.ts +38 -0
- package/lib/index.js +378 -0
- package/lib/menu-filter.d.ts +134 -0
- package/lib/menu-filter.js +428 -0
- package/lib/menu-visibility.d.ts +26 -0
- package/lib/menu-visibility.js +77 -0
- package/lib/queue-dock.d.ts +85 -0
- package/lib/queue-dock.js +291 -0
- package/lib/queue.d.ts +128 -0
- package/lib/queue.js +313 -0
- package/lib/reactive.d.ts +57 -0
- package/lib/reactive.js +75 -0
- package/lib/reasoning-efforts.d.ts +120 -0
- package/lib/reasoning-efforts.js +143 -0
- package/lib/routes.d.ts +126 -0
- package/lib/routes.js +267 -0
- package/lib/settings-ui.d.ts +183 -0
- package/lib/settings-ui.js +367 -0
- package/lib/visibility-settings.d.ts +193 -0
- package/lib/visibility-settings.js +225 -0
- package/lib/visibility.d.ts +152 -0
- package/lib/visibility.js +235 -0
- package/package.json +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 leaves chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# @leaves615/dsh-llm-ctl
|
|
2
|
+
|
|
3
|
+
[](https://github.com/leaves615/dsh-llm-ctl/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@leaves615/dsh-llm-ctl)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
[功能概览](#功能概览) · [工作原理](#工作原理) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)
|
|
8
|
+
|
|
9
|
+
## 功能概览
|
|
10
|
+
|
|
11
|
+
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)
|
|
12
|
+
web profile 的准入管制 + 模型隐藏。
|
|
13
|
+
|
|
14
|
+
- **限流排队。** 所有 `llm/stream` 调用——agent 主循环、标题生成、压缩后台任务——
|
|
15
|
+
先拿 per-provider 槽位再放行。严格 FIFO,per-provider 并发上限,单个等待预算
|
|
16
|
+
(`maxWaitMs`)。终端限流失败后整个 provider 冷却(优先 `Retry-After` /
|
|
17
|
+
`providerRetryAfterMs`,没有就本地指数退避);排队的请求等着而不是 429 炸掉。
|
|
18
|
+
超预算 → `QUEUE_TIMEOUT`,队满 → `QUEUE_FULL`,输入框排队 pill 里每条可单独取消。
|
|
19
|
+
- **独立自愈。** `agent/request-error` 上先登记冷却,再问下游:`dsh-llm-retry`
|
|
20
|
+
接管就透传,没人管就花自己的有界预算(`reactiveRetry: auto`,默认 3 次)——
|
|
21
|
+
没装 retry 插件的 profile 第一个 429 也不炸轮。
|
|
22
|
+
- **模型隐藏。** provider / 单模型两级开关,持久化到 `llm-ctl` settings 分区;
|
|
23
|
+
`hiddenPatterns` 预置;设置页卡片 + 页脚;模型菜单过滤 + 自带搜索框
|
|
24
|
+
(`p:` 前缀按 provider 过滤);全隐空态一键恢复;默认模型被藏后自动回退。
|
|
25
|
+
- **上游发现。** 每张 provider 卡上的刷新按钮重发现上游模型清单(先走 adapter
|
|
26
|
+
发现,服务端用存好的 credential;zen 系失败时回退公开 Zen feed)。
|
|
27
|
+
密钥不过浏览器通道——discover 请求里带 `apiKey` 直接 HTTP 400。
|
|
28
|
+
|
|
29
|
+
跟另外两个插件的关系:`dsh-llm-retry` 是执行器(在 durable 步骤边界重跑失败请求);
|
|
30
|
+
本插件是门卫(发出去之前排队,限流后让整个 provider 歇会儿)。
|
|
31
|
+
`dsh-model-search-plugin` 只搜索不隐藏;本插件只隐藏——对方在场时搜索框自动让路。
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
dsh plugin --profile web add -w @leaves615/dsh-llm-ctl
|
|
35
|
+
dsh web # 重启加载
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
前置条件:DSH web profile,Node.js >= 22。在
|
|
39
|
+
`@deepseek-ai/dsh-llm 0.1.2-rc.1` 上测过。headless 也能加载
|
|
40
|
+
(排队 + 自愈正常工作,缺 `webServer` 时菜单/dock UI 休眠)。
|
|
41
|
+
|
|
42
|
+
## 工作原理
|
|
43
|
+
|
|
44
|
+
拿 zen-free 举例。你把它的并发设成 1,同时开了 5 个任务:第 1 个先跑,剩下 4 个排队。第 1 个撞上 429,还带了 `Retry-After: 12s`——整个 zen-free 冷却 12 秒,排队的 4 个一起等,输入框上方出现「排队 4 · ~12s」,点开每条都能单独取消。12 秒后按排队顺序一个个放行。如果要等的时间超过预算,不等,直接失败告诉你。
|
|
45
|
+
|
|
46
|
+
插件卡在三个接缝上:
|
|
47
|
+
|
|
48
|
+
| 接缝 | 职责 |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `llm/stream`(全局 prepend) | 所有调用——agent 主循环、标题生成、压缩后台任务——按 provider 取到槽位才放行。排队时还没碰 provider,请求只读不改 |
|
|
51
|
+
| `agent/request-error`(全局 prepend) | 先登记冷却,再问下游。有 retry 插件接管就透传,没人管就自己花有界预算重试 |
|
|
52
|
+
| 设置页 + 模型菜单 + 输入框状态条 | provider 卡开关、菜单过滤、排队 pill,浏览器半走 `/api/llm-ctl/*` 跟 host 说话 |
|
|
53
|
+
|
|
54
|
+
## 安装
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
dsh plugin --profile web add -w @leaves615/dsh-llm-ctl # 或:add -w /path/to/dsh-llm-ctl
|
|
58
|
+
dsh web # 重启加载
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`dsh.bundle.patch` 和 `dsh.client` 都已声明,host 和浏览器两半自动装好,不用手改 `cordis.patch.yml`。
|
|
62
|
+
|
|
63
|
+
### 前置条件
|
|
64
|
+
|
|
65
|
+
- DSH(含 web profile),Node.js >= 22。
|
|
66
|
+
- 在 `@deepseek-ai/dsh-llm 0.1.2-rc.1` 上测过;peer 依赖 `@deepseek-ai/cordis ^4.0.2`。
|
|
67
|
+
- headless 也能加载:排队 + 自愈正常工作,只是没有状态条和菜单过滤(缺 `webServer` 时浏览器半休眠)。
|
|
68
|
+
|
|
69
|
+
## 安装之后
|
|
70
|
+
|
|
71
|
+
什么都不配就能用:默认不限流,自愈开着,隐藏规则为空。先去两个地方看一眼:
|
|
72
|
+
|
|
73
|
+
1. 模型设置页——每个 provider 多出一张卡,开关控制显示和隐藏,底部有隐藏总数和全部恢复。
|
|
74
|
+
2. 设置 → 插件 → 插件配置——`llm-ctl` 卡,排队预算都在这里改,保存即生效,不用重启。
|
|
75
|
+
|
|
76
|
+
## 配置
|
|
77
|
+
|
|
78
|
+
完整配置长这样,每项都有默认值,不写就是下面这个效果:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
- id: llm-ctl
|
|
82
|
+
name: '@leaves615/dsh-llm-ctl'
|
|
83
|
+
config:
|
|
84
|
+
queue:
|
|
85
|
+
# 每个 provider 几个并发。不写 default 就是全不限流,0 也是不限流。
|
|
86
|
+
perProviderConcurrency: { 'zen-free': 1 }
|
|
87
|
+
maxQueueDepth: 50 # 队满报 QUEUE_FULL
|
|
88
|
+
# 唯一的等待预算:排队愿意等多久,也愿意接受多长的冷却。
|
|
89
|
+
# 明确要等更久就直接报 QUEUE_TIMEOUT,不白等。
|
|
90
|
+
maxWaitMs: 120000
|
|
91
|
+
honorRetryAfter: true # provider 的提示优先于本地退避
|
|
92
|
+
backoff: { initialDelayMs: 500, maxDelayMs: 10000, jitterRatio: 0.1 }
|
|
93
|
+
# 没装 dsh-llm-retry 时的兜底:auto = 下游没人管才自己上(默认 3 次);
|
|
94
|
+
# off = 从不;数字 = 每步最多几次。
|
|
95
|
+
reactiveRetry: auto
|
|
96
|
+
visibility:
|
|
97
|
+
# 启动预置的隐藏规则,用户层只读。通配符只有 '*',大小写不敏感,
|
|
98
|
+
# 'provider:model' 或裸模型名都行。
|
|
99
|
+
hiddenPatterns: ['*-test-*']
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
排队是严格 FIFO,先到先得,不分前后台优先级。
|
|
103
|
+
|
|
104
|
+
触发冷却的失败码跟 dsh-llm-retry 默认集一致:`RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT / EMPTY_RESPONSE`。直接抛错的算数,包在 SSE 流里回来的 terminal finish chunk(OpenRouter 转发上游 429 就是这样)也算数。`AUTH`、`QUOTA` 这类码不排队,原样透传。
|
|
105
|
+
|
|
106
|
+
等多久听 provider 的:优先用 adapter 透出来的 `failure.providerRetryAfterMs`(DeepSeek 的 adapter 已经给了),没有就本地指数退避。
|
|
107
|
+
|
|
108
|
+
> 上游进展:`Retry-After`、`x-ratelimit-reset` 这些 header 的解析器已经写好测过,就等 adapter 把原始响应头放进 `LlmError.details`,接上就能用,插件这边已经收 `headers` 包了。
|
|
109
|
+
|
|
110
|
+
## 模型隐藏
|
|
111
|
+
|
|
112
|
+
两级开关:整个 provider 藏(`providers.<id>`),单个模型藏(`models.<provider:model>`),再加启动预置的 `hiddenPatterns`。判定顺序:provider 全藏 > 单模型指定 > provider 显式放行 > 预置规则 > 可见。
|
|
113
|
+
|
|
114
|
+
开关存在 settings 的 `llm-ctl` section,user 层持久化,换机器还在。设置页把所有声明过的 provider 都列出来,catalog 空的、过期的也有开关;每张卡上有刷新按钮,能重新发现上游模型(服务端用存好的 credential,不碰 secret),新模型挂 "new" 徽标。藏掉默认模型时,默认会自动挪到第一个可见的。
|
|
115
|
+
|
|
116
|
+
菜单这边:装了 `dsh-model-search-plugin` 就不抢搜索框,只剔除隐藏项;没装则自己补一个,支持 `p:` 前缀(`p:zen flash` 按 provider 过滤),按键不冒泡出去。全被滤掉时有一键恢复入口。
|
|
117
|
+
|
|
118
|
+
## 不做的事情
|
|
119
|
+
|
|
120
|
+
- 不做计费和配额统计,不做 token 级限流。
|
|
121
|
+
- 不改请求内容,不碰 adapter 的协议逻辑。
|
|
122
|
+
- 不做跨进程分布式队列,单进程内存队列够用了。
|
|
123
|
+
- **不写别人的配置。** 本插件只写自己的 `llm-ctl` section,`llm-pi-ai` 等别的插件的命名空间一律只读。想给自定义模型补思维等级选择器,去改它自己的配置,别指望本插件代笔(原因见 `notes/reasoning-effort-gap.md` 的下架决策)。
|
|
124
|
+
|
|
125
|
+
## 已知问题
|
|
126
|
+
|
|
127
|
+
服务端 `session.modelCatalog` 目前拦不住(没有 waterfall 接缝),"隐藏模型不出 host"需要上游加接缝,过滤发生在浏览器侧。
|
|
128
|
+
|
|
129
|
+
也不写自定义会话日志事件:`Session.append()` 给不了站外事件 `ignorable` 标记,硬写会让持久化读路径拒绝重建会话。可观测性只有 host 日志和 `llmCtl` Remote 暴露的内存环,等上游开放标记再说。
|
|
130
|
+
|
|
131
|
+
## 常见问题
|
|
132
|
+
|
|
133
|
+
**`QUEUE_FULL` / `QUEUE_TIMEOUT` 是什么意思?**
|
|
134
|
+
`QUEUE_FULL` = 排队数超过 `maxQueueDepth`(默认 50),新请求直接拒绝,稍后再试或调大队列。`QUEUE_TIMEOUT` = 要等的时间超过 `maxWaitMs`(默认 120s)——已知等不起就立刻失败,不白等;调大 `maxWaitMs` 或降低并发需求可缓解。
|
|
135
|
+
|
|
136
|
+
**排队能取消吗?**
|
|
137
|
+
能。输入框上方的排队 pill 点开,每条请求可单独取消;取消记 `cancelled` 事件,立即释放槽位。
|
|
138
|
+
|
|
139
|
+
**`SETTINGS_CONFLICT` 保存失败?**
|
|
140
|
+
多人/多窗口同时改 `llm-ctl` 配置会撞 revision。刷新设置页重读最新 revision 再保存即可;插件内部写配置自带冲突重试。
|
|
141
|
+
|
|
142
|
+
**卸载 / 回滚?**
|
|
143
|
+
`dsh plugin --profile web remove @leaves615/dsh-llm-ctl` 后重启。`llm-ctl` settings section 残留的开关数据不影响其他插件,清理可手动删除该 section。
|
|
144
|
+
|
|
145
|
+
**跟 `dsh-llm-retry` 一起装会打架吗?**
|
|
146
|
+
不会。`agent/request-error` 上 retry 插件接管时本插件只透传(零重复计数);只有下游无动作(void)时才花自己的有界预算。装/卸 retry 插件无需改本插件配置。
|
|
147
|
+
|
|
148
|
+
## 本地开发
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
npm install
|
|
152
|
+
npm run verify # typecheck + build + tests + 真实 loader smoke boot
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`npm run typecheck` 最快,先跑它。`scripts/smoke-boot.sh` 在 `.dsh-home/` 下组装临时 profile(不碰 `~/.dsh`),断言 `llm-ctl` 行装上再随机端口起服。
|
|
156
|
+
|
|
157
|
+
测试直接跑 TS 源码(Node 类型剥离),所以 host 半没用装饰器语法,Typert `Remote` 标记是在 `src/controller.ts` 里编程式挂的。
|
|
158
|
+
|
|
159
|
+
| 文件 | 职责 |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `src/index.ts` | 入口:两个 waterfall listener、预算、remote controller |
|
|
162
|
+
| `src/queue.ts` | per-provider 并发、FIFO、冷却时钟、ETA |
|
|
163
|
+
| `src/delay.ts` | 延迟阶梯和 header 解析 |
|
|
164
|
+
| `src/reactive.ts` | 兜底重试的决策、预算、可中断等待 |
|
|
165
|
+
| `src/config.ts` | schema、默认值、并发解析 |
|
|
166
|
+
| `src/controller.ts` + `src/routes.ts` | `ctx.remote.llmCtl` 和 `/api/llm-ctl/*` |
|
|
167
|
+
| `src/settings-ui.ts` | 设置页 provider 卡、footer、插件配置卡 |
|
|
168
|
+
| `src/menu-filter.ts` | 模型菜单 DOM 过滤(选择器只留在这里) |
|
|
169
|
+
| `src/menu-visibility.ts` | 外部搜索插件共存:探测到对方搜索框时只隐藏不重写 |
|
|
170
|
+
| `src/queue-dock.ts` | 输入框上方的排队状态条 |
|
|
171
|
+
| `src/visibility.ts` + `src/visibility-settings.ts` | 开关解析/过滤/回退 + settings section 适配层 |
|
|
172
|
+
| `src/discover.ts` + `src/discover-ui.ts` | 上游模型发现(adapter 优先,zen feed 兜底)+ 发现列表视图 |
|
|
173
|
+
| `src/concurrency.ts` + `src/events.ts` | 并发解析 + 有界内存控制面日志 |
|
|
174
|
+
| `src/client-plugin.ts` | 浏览器半:状态条、菜单同步、HTTP 轮询 |
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# dsh-llm-ctl bundle patch: one insert row for the host half.
|
|
2
|
+
# The host half owns the llm/stream admission queue and the agent/request-error
|
|
3
|
+
# standalone recovery; the browser half ships through exports["./client"] and is
|
|
4
|
+
# discovered by the package.json dsh.client declaration.
|
|
5
|
+
- insert:
|
|
6
|
+
- id: llm-ctl
|
|
7
|
+
name: '@leaves615/dsh-llm-ctl'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Cordis service names the browser half needs. */
|
|
2
|
+
export declare const inject: string[];
|
|
3
|
+
interface CatalogModel {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
interface CatalogGroup {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
models: readonly CatalogModel[];
|
|
11
|
+
}
|
|
12
|
+
interface ModelCatalog {
|
|
13
|
+
groups: readonly CatalogGroup[];
|
|
14
|
+
failures?: readonly {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
interface RemoteResult<T> {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
value?: T;
|
|
23
|
+
error?: {
|
|
24
|
+
code: string;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
interface ClientContext {
|
|
29
|
+
remote: {
|
|
30
|
+
session: {
|
|
31
|
+
modelCatalog(): Promise<RemoteResult<ModelCatalog>>;
|
|
32
|
+
};
|
|
33
|
+
llm?: {
|
|
34
|
+
listConfigurableProviders(): Promise<RemoteResult<Array<{
|
|
35
|
+
provider: string;
|
|
36
|
+
displayName: string;
|
|
37
|
+
settingsNs: string;
|
|
38
|
+
}>>>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
slots?: {
|
|
42
|
+
inject(key: string, callback: () => (() => void) | Iterable<() => void> | void): () => void;
|
|
43
|
+
register(options: Record<string, unknown>, component: unknown): () => void;
|
|
44
|
+
};
|
|
45
|
+
logger?: {
|
|
46
|
+
warn(...args: unknown[]): void;
|
|
47
|
+
};
|
|
48
|
+
effect?(callback: () => (() => void) | void, label?: string): void;
|
|
49
|
+
}
|
|
50
|
+
/** Owner props dispatched by the settings-models provider-card seat. */
|
|
51
|
+
interface ProviderCardOwnerProps {
|
|
52
|
+
provider?: {
|
|
53
|
+
provider?: string;
|
|
54
|
+
displayName?: string;
|
|
55
|
+
};
|
|
56
|
+
configured?: boolean;
|
|
57
|
+
keyConfigured?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the provider a seat instance controls.
|
|
61
|
+
*
|
|
62
|
+
* Exported for tests: shared-namespace rows must resolve from owner props.
|
|
63
|
+
*
|
|
64
|
+
* @param owner Owner props dispatched by the slot; may be absent in tests.
|
|
65
|
+
* @param fallbackProviderId Registration-time provider id, last resort only.
|
|
66
|
+
* @returns The provider id every switch in this seat writes.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveSeatProvider(owner: ProviderCardOwnerProps | undefined, fallbackProviderId: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* Backoff ladder for failed polls: 1s, 2s, 4s … capped at 30s, so a
|
|
71
|
+
* struggling host is not hammered every second (each failed fetch also logs
|
|
72
|
+
* a console "Failed to load resource" line, which is the 503 spam).
|
|
73
|
+
*
|
|
74
|
+
* Exported for tests: the ladder is the contract that bounds poll pressure.
|
|
75
|
+
*
|
|
76
|
+
* @param failures Consecutive fetch failures.
|
|
77
|
+
* @returns Delay in ms before the next poll.
|
|
78
|
+
*/
|
|
79
|
+
export declare function pollDelayForFailures(failures: number): number;
|
|
80
|
+
/** Start the browser half. */
|
|
81
|
+
export declare function apply(ctx: ClientContext): void;
|
|
82
|
+
export {};
|