dsh-llm-rate-limiter
Đã xác minh@leaf233/dsh-llm-rate-limiter · v0.3.0 · MIT · Giao diện web
Per-model LLM call rate limiter for DeepSeek Harness with queue support and a live status panel
Cài đặt
dsh plugin add @leaf233/dsh-llm-rate-limiter Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.
Mã nguồn
Thẻ
Tác giả
Readme
dsh-llm-rate-limiter
English | 中文
DeepSeek Harness 的按模型 LLM 调用限速插件,支持排队/拒绝两种节流策略,并带交互式图形配置界面。
功能
- 按模型限速 —— 每个
provider/model独立的并发数、RPM 与突发容量限制 - 两种算法 —— 令牌桶(允许突发)或滑动窗口(平滑、严格 RPM)
- 排队模式 —— 被限流的请求排队等待,有槽位空出时放行
- 拒绝模式 —— 被限流的请求立即失败(可与
dsh-llm-retry配合自动退避重试) - 交互式界面 —— DSH 设置 → 插件页内的可展开配置卡片
- 实时状态面板 —— 实时计数器、按模型的进度条与事件日志(v0.2.0 起)
- 热更新 —— 配置改动立即生效,无需重启
- 全量检查 —— 拦截
llm/streamwaterfall,覆盖每个 agent 轮次里的每一次 LLM 调用
状态面板(v0.2.0)
打开卡片后,其正文顶部会显示实时面板:
📊 实时状态 ● 实时 [清零]
请求 42 · 通过 38 · 拒绝 2 · 超时 1 · 中止 1 · 平均等待 214ms
deepseek/deepseek-chat [令牌桶] ▓▓▓▓▓▓▓░░░ 7.5/10 并发 2/5 排队 1
openai/gpt-4o [滑动窗口] ▓▓▓▓▓▓▓▓▓▓ 3/3 rpm 并发 1/5
12:00:03 timeout openai/gpt-4o 等待 1m
12:00:01 rejected openai/gpt-4o
12:00:00 granted deepseek/deepseek-chat 等待 4.2s
| 方面 | 行为 |
|---|---|
| 数据通道 | 通道 /llm-rate-limiter —— 带认证(401/403 栅栏),POST+JSON,随插件 fiber 自动清理 |
| 载体 | 优先用框架的 connection.rpc.handle();框架路径失效时,退回到插件自注册的前缀路由并复用 connection.requestRejection()(见下文) |
| 轮询节奏 | 卡片展开时 1 秒轮询;连续失败后退避 2 秒 → 4 秒 → 8 秒 |
| 折叠状态 | 面板卸载,因此完全不轮询 |
| 端点 | snapshot(实时计数)与 reset(清零统计) |
| 通道不可用时 | 显示「状态通道不可用」,卡片其余部分功能不受影响 |
| 计数器 | requests / granted / rejected / timeouts / aborted / totalWaitMs,外加最近 8 条事件(环形缓冲为 64) |
| 进度条 | 令牌桶显示 tokens/burstSize;滑动窗口显示 countInWindow/maxRpm;接近上限时变琥珀色 |
载体兜底(DSH 0.1.5-rc.3 起)
DSH 0.1.5-rc.3 把 @deepseek-ai/dsh-client-connection 这个框架插件自身的 inject 从
["webServer", "credentials"] 改成了 ["credentials"],但其
HostConnectionService.register() 仍然解引用 owner.webServer。
Cordis 会把跨 fiber 读取的 service 的 ctx 重绑定到读取者的 fiber,因此
connection.rpc.handle() 会抛错:在 0.1.7-rc.1 中依然存在 —— 见下文。
cannot get property "webServer" without inject
插件会检测到该错误,并自行挂载同一条通道:
| 路径 | 触发条件 | 做法 |
|---|---|---|
| 1(首选) | connection.rpc.handle() 正常 |
由框架持有路由、请求校验与 fiber 级撤销 |
| 2(兜底) | 路径 1 抛错 | 插件在自己的 fiber(能看到 webServer)注册 kind: "prefix" 路由,并复用 connection.requestRejection() 作为 403/401 栅栏 |
两条载体使用逐字相同的 wire 协议,因此浏览器端无需改动 —— 面板也分辨不出
当前是哪一条。若 connection.requestRejection() 不可用,插件会拒绝挂载,
而不是发布一条未鉴权的路由。
安装
方式 1:npm(推荐)
dsh plugin add <your-profile> @leaf233/dsh-llm-rate-limiter
# 或在该 profile 目录内:
pnpm add @leaf233/dsh-llm-rate-limiter
方式 2:本地路径(开发)
dsh plugin add <your-profile> ./path/to/dsh-llm-rate-limiter
# 或
dsh plugin add ./path/to/dsh-llm-rate-limiter # 默认 profile
插件必须被加入该 profile 的
package.json依赖中。 bundle 入口(cordis.patch.yml)由reconcilePlugins自动识别。0.3.0 要求 DSH 0.1.7-rc.1 或更新版本。 若使用 DSH 0.1.5,请锁定
@leaf233/[email protected]。见兼容性表。
方式 3:从 GitHub 安装
dsh plugin add <your-profile> github:Leafyezi233/dsh-llm-rate-limiter
⚠️ 重要:从 Git 安装的插件在首次安装时会被 pnpm 的
allowBuilds限制拦下。 若安装失败,请查看报错信息中 pnpm 给出的确切键名,然后加入该 profile 的pnpm-workspace.yaml:pnpm: allowBuilds: - '@leaf233/dsh-llm-rate-limiter'然后重新执行安装命令。
配置
通过图形界面
- 打开 DSH Web UI(
dsh web) - 进入设置 → 插件,展开 Official 分组
- 找到 LLM 调用限速 卡片(
data-plugin-item="llm-rate-limiter")并打开 - 配置全局默认值、按模型覆盖与节流行为 —— 改动经页面自带的表单保存, 实时状态面板就在同一页
卡片出现在 Official 分组里,是因为 DSH 把所有
plugins.item的注册者 都列在那里;本插件的order为900,因此排在五个官方设置页之后。
通过配置文件
改动会被持久化到该 profile 的 Cordis patch 中该条目的 config 里
(0.1.7 起,插件以自己的 Config 作为配置来源;旧版 settings.yaml 由
DSH 在首次启动时导入一次并改名)。形状如下:
llm-rate-limiter:
enabled: true
strategy: token-bucket # "token-bucket" | "sliding-window"
defaults:
maxConcurrent: 5
maxRpm: 60
burstSize: 10 # 仅令牌桶
refillRate: 1 # 仅令牌桶(每秒补充的令牌数)
models:
"deepseek/deepseek-chat":
maxConcurrent: 8
maxRpm: 120
"openai/gpt-4o":
maxConcurrent: 2
maxRpm: 10
burstSize: 3
"anthropic/claude-3-5-sonnet":
enabled: false # 对该模型跳过限速
onThrottled: queue # "queue" | "reject"
maxQueueWaitMs: 60000
设置项参考
| 字段 | 默认值 | 说明 |
|---|---|---|
enabled |
true |
全局总开关。关闭时零开销直接放行。 |
strategy |
"token-bucket" |
"token-bucket"(允许突发)或 "sliding-window"(平滑、严格 RPM) |
defaults.maxConcurrent |
5 |
每个模型的最大同时请求数 |
defaults.maxRpm |
60 |
每个模型每分钟最大请求数 |
defaults.burstSize |
10 |
令牌桶容量 —— 一次最多可突发多少个请求 |
defaults.refillRate |
1 |
每秒补充的令牌数(令牌桶)。未设置时由 maxRpm / 60 自动推导。 |
models.<key>.maxConcurrent |
— | 该模型的并发数覆盖 |
models.<key>.maxRpm |
— | 该模型的 RPM 覆盖 |
models.<key>.burstSize |
— | 该模型的突发容量覆盖 |
models.<key>.refillRate |
— | 该模型的补充速率覆盖 |
models.<key>.enabled |
— | 设为 false 可对该模型跳过限速 |
onThrottled |
"queue" |
请求触限时的行为:"queue"(等待)或 "reject"(立即失败) |
maxQueueWaitMs |
60000 |
请求在队列中等待多久(毫秒)后放弃并被拒绝 |
注意:当模型覆盖了
maxRpm但未显式设置refillRate时,补充速率会 自动按maxRpm / 60(每秒令牌数)推导。这样才能保证「设 maxRpm=3」真的限制到 每分钟 3 个请求。
算法对比
| 令牌桶 | 滑动窗口 | |
|---|---|---|
| 突发 | 允许(由 burstSize 控制) |
不允许 —— 严格平滑 |
| 恢复 | 令牌按 refillRate/秒补充 |
窗口持续滑动 |
| 最适合 | 容忍请求尖峰 | 有硬性每分钟上限的 API |
| 界面名称 | 令牌桶 (Token Bucket) | 滑动窗口 (Sliding Window) |
工作原理
Agent 轮次
→ LLM 调用(例如 deepseek/deepseek-chat)
→ ctx.on("llm/stream") 拦截器
→ 为该 provider/model 解析对应的限速器
→ 令牌桶:还有令牌且并发未满?
→ 是:消耗令牌、占用槽位、转发到 API
→ 否(拒绝模式):立即返回 RATE_LIMIT 错误
→ 否(排队模式):进入 waiters[] 等待令牌补充
→ 请求完成 → 释放槽位 → 唤醒等待中的请求
→ dsh-llm-retry 捕获 RATE_LIMIT → 指数退避 → 重试
开发
# 克隆
git clone https://github.com/Leafyezi233/dsh-llm-rate-limiter.git
cd dsh-llm-rate-limiter
# 安装依赖
pnpm install
# 跑完整测试套件(6 个文件,375 条断言)
pnpm test
# 单独运行各套件
node test-strategies.mjs # 19 —— 限速算法
node test-status-rpc.mjs # 61 —— 通道处理器与计数器
node test-client-bundle.mjs # 106 —— 真实客户端 bundle 跑在迷你 React 运行时上
node test-host-integration.mjs # 80 —— 真实 apply(ctx, config) 接线与 volatile 引用
node test-status-route.mjs # 73 —— 自注册路由与 0.1.5 回归
node test-config-schema.mjs # 36 —— 设置页所依赖的 Config volatile 契约
# 针对真实 DSH 安装的实机 HTTP 验证(真实 socket、真实 webserver)
pnpm run verify:live # 19 —— 无 DSH 时以退出码 2 跳过
# 运行端到端限速测试
node test-3rpm.mjs
# 安装到某个 DSH profile 里测试
dsh plugin add <your-profile> .
通过 link: 安装时,插件走实时符号链接 —— 对 lib/ 的改动在浏览器硬刷新
(Ctrl+Shift+R)后即生效,无需重新安装。
兼容性
| DSH 版本 | 插件版本 | 状态 | 说明 |
|---|---|---|---|
| 0.1.2-rc.1 | 0.2.x | ✅ 已测 | 最初目标;connection.rpc 载体 |
| 0.1.5-rc.3 | 0.2.x | ✅ 已测 | 需要载体兜底;已通过实机 HTTP 验证 |
| 0.1.7-rc.1+ | 0.3.x | ✅ 已测 | settingsScope → configForms;settings.plugin.item → plugins.item;基于 Config 的设置 |
| 0.2.x(DSH) | — | ⚠️ 未测 | 可能需要调整 API |
| Cordis 5+ | — | ⚠️ 未测 | 大版本升级多半需要重写 |
0.3.0 的破坏性变更
DSH 0.1.7 移除了本插件设置界面所用的两个客户端服务:
| 0.1.5 | 0.1.7 |
|---|---|
settingsScope 服务 |
configForms 服务 |
settings.plugin.item 槽位 |
plugins.item 槽位 |
宿主 ctx.settings.register(ns, schema, { base }) |
条目自己导出的 Config schema |
宿主 scope.get() / scope.watch() |
.volatile() 引用,用 .get() 读取 |
0.3.0 只支持 DSH 0.1.7-rc.1+。使用 0.1.5 请安装 0.2.x。
本次迁移所依赖的四个断裂点,已就地写在 lib/index.js 与 lib/types/config.js
的注释里;完整推理记录在 CHANGELOG.md 的 [0.3.0] 段落。