Skip to content
dsh.fish
Bundle

dsh-effort-router

DSH Effort Router(模型分流):按每一轮请求的难度自动选择模型与思考强度——简单问题走便宜快模型,难题才叫强模型。规则判定零 token,灰区交给一次小模型裁定,图片档强度随难度升降(low/high/max)。每轮请求级覆盖,不改你的会话选择与默认模型。

Source
wanghaixu-hai
License
MIT
Updated
Updated 14 hours ago

Readme

# 🧭 dsh-effort-router

> **给 DeepSeek Harness 装一个"按题目难度自动换模型"的路由器。**
> 简单问题走便宜快模型,难题才叫强模型;规则判定 **零 token**,只有判不准的灰区才问一次小模型。
> **发送前会把本轮计划同步到输入框那个模型框**,所以座位显示的就是这一轮真正会用的模型。

[English summary ↓](#english-summary) · [更新日志](CHANGELOG.md) · License: MIT

---

## 这是什么?

这是一个 **DSH 插件**(DeepSeek Harness plugin),包名 `dsh-effort-router`。DSH 的插件就是普通的 npm 包,通过 `package.json` 里的 `dsh` 字段向宿主声明自己:

```jsonc
{
  "name": "dsh-effort-router",
  "dsh": {
    "bundle": { "patch": "./cordis.patch.yml" },   // 往宿主插件树里插一行:{ id: model-router, name: dsh-effort-router }
    "client": { "platform": "web", "inject": [...] } // 另半个自己在浏览器里跑(输入框开关、设置面板)
  }
}
```

安装到某个 profile 之后,它会挂在 `agent/request` 这条请求水路上,**在每次真正调用模型之前决定这一轮用哪个 provider / 模型 / 思考强度**。

---

### 关于名字(重要)

这个插件叫 **`dsh-effort-router`**,中文名仍是「模型分流」。原因很直白:**npm 上的 `dsh-model-router` 是别人的包**(`thedeveloper256`,已发到 0.7.0,是「角色制路由」,与本项目无关)。npm 不允许重名发布,所以:

- **不要**执行 `dsh plugin add dsh-model-router` —— 那会装上别人的插件;
- 用 **tarball**(`dsh plugin add /path/to/dsh-effort-router-0.3.0.tgz`)或从本仓库安装;
- GitHub 仓库名保留 `dsh-model-router`(改名可选,不影响使用)。

本插件的内部标识保持原样:HTTP 前缀 `/api/model-router`、插件树 entry id `model-router`、设置页 tab id。只有**包名**变了。

## 它解决什么问题

| 你的痛点 | 手动做法 | 这个插件 |
|---|---|---|
| 问个「1+1」也烧着最贵的模型 | 每次手动切 | 自动判 simple → 便宜快模型 + 低思考强度 |
| 全程用便宜模型,难题质量掉 | 猜什么时候该切 | 自动判 hard → 强模型 + MAX 思考强度 |
| 贴一张报错截图,模型不会看图 | 先手动换 vision 模型 | 图片档独立成档,思考强度按难度给(low/high/max) |
| 切来切去很烦,还怕改了自己的设置 | — | **每轮请求级覆盖**:不动你的会话选择、不动你的全局默认模型 |

一句话:**把"该用多贵的脑子"这件事自动化,且不留下副作用。**

---

## 30 秒上手

```bash
# 从 tarball 安装(把路径换成你下载到的文件)
dsh plugin --profile web add /path/to/dsh-effort-router-0.3.0.tgz

# 或从源码目录安装
dsh plugin --profile web add /path/to/dsh-effort-router
```

然后 **重启宿主**(host 半边是 Node 进程加载的,必须重启;client 半边只需刷新页面)。

开机后你会看到:

- 输入框**右侧**多了一个开关:`🧭 分流:开 / 关` —— 点一下即时生效,不用进设置、不用找保存按钮;
- 输入框**下方**多了一行读数带:`🧭 本轮实际 deepseek-v4-flash@high · 档位 standard`;
- **设置 → 插件 → 🧭 模型分流** 里有完整面板:分档映射、图片档强度、灰区判定、分类测试、最近决策。

---

## 工作原理

### 四个档位

| 档位 | 何时命中 | 默认目标(按你的模型目录自动选) |
|---|---|---|
| ① simple | 闲聊、单点问答、小改动 | 便宜快模型 @ low |
| ② standard | 常规问答(规则判不准时交给小模型裁定) | 便宜快模型 @ high |
| ③ hard | 原理/机制/架构/多步/长文/代码块 | 强模型 @ max |
| ④ vision | 本轮带图 | 能收图的模型,强度按难度 **low / high / max** |

档位映射可以完全手写覆盖(面板下拉选),留空即用"自动推荐"。

### 判定规则(零 token)

**加分(越难越高)**

| 信号 | 权重 |
|---|---|
| `为什么 / 原理 / 本质 / 深层 / 底层 / 机制 / 论证 / 证明` | +2 |
| `架构 / 设计 / 重构 / 迁移 / 优化 / 调优 / 方案 / 对比 / 取舍 / 选型` | +2 |
| `并发 / 分布式 / 一致性 / 事务 / 缓存 / 索引 / 性能 / 安全 / 漏洞` | +2 |
| 代码块 ``` | +2 |
| `帮我做 / 实现一个 / 开发一个 / 搭建 / 集成 / 部署一整套` | +2 |
| `多个 / 分步 / 先…再…最后 / 步骤 / 流程 / 全流程 / 端到端` | +1 |
| 文本 >800 字 → +2;>300 字 → +1 | — |

**减分(越像小事越低)**:以 `你好/在吗/谢谢/好的/ok/hi` 开头 −3;`是什么/多少/几/谁/哪里/哪年` −1;`改一下/换成/删掉/加上/重命名` −1;短于 30 字 −1;≤16 字且非疑问句再 −1。

**图片信号**:带图 + 配文含 `报错/日志/代码/修复/排查/定位/500` 等 +2;多张图 +1;只有图没配文 +1(不会再被当成"小事")。

合计 `≥3` → hard,`≤−2` → simple,其余 standard。

### 灰区判定(判不准时才花一次小调用)

规则落在 **standard** 这个最不可靠的中间带时,插件会把这一轮交给一个便宜模型判一次难度(`maxTokens: 8`、4 秒超时、失败即退回规则)。明显简单和明显困难的轮次**一次调用都不花**——所以像「为什么这段会内存泄漏?`setInterval(()=>{},100)`」这种"差 1 分"的句子,也能自己走到 MAX,**不需要你维护任何关键词表**。

### 每轮请求级覆盖

```text
你发消息 ──> 客户端(开关 / 计划同步 / 读数带)
                 │
                 ▼
           宿主 agent/request 水路
                 │  ① 分类:规则 →(灰区)小模型
                 │  ② 记忆:手选?我们设过的?会话开局?
                 │  ③ 覆盖这一次请求的 provider/model/reasoningEffort
                 ▼
             实际模型调用(你的会话选择不变)
```

---

## 开关与取舍(重要,请先读这段)

| 开关 | 默认 | 作用 | 代价 |
|---|---|---|---|
| **启用自动分流** | 开 | 总开关。关掉后宿主直接放行,**完全不碰**任何东西 | — |
| **尊重手动选择** | 开 | 你手动在座位上选过的模型,插件让开(决策日志里记 `skip:manual`) | — |
| **记录决策日志** | 开 | 每次判定写一行 JSONL | 极小磁盘 |
| **灰区判定** | 开 | 见上,让"不靠关键词"成为可能 | standard 轮多一次小调用(约 8 token 输出) |
| **座位同步** | 无开关,始终生效 | 发送前把本轮计划写进会话选择:座位框显示的就是本轮真正要用的模型,带图轮自动落到图片档(所以文本模型会话也能直接发图) | 每轮一次会话选择写入——座位会跟着计划走,这正是设计目标;全局默认模型先读走再写回,不会被动 |

几个开关点一下**立即生效**(立即写盘 + 让宿主的计划缓存失效),不需要滚到面板底部再点保存。

### 座位 = 本轮实际(这是设计,不是巧合)

模型座位渲染的是**会话选择**,而分流是**每轮请求级**决定的。为了不让两者打架,插件在每次发送前多做一步:

```text
你按下发送
   │
   ├─ POST /api/model-router/preview   本轮的文字 + 图片张数(与正式判定同一套代码路径)
   │     返回:目标模型@强度,以及"这是不是你自己手选过的"
   ├─ 你没手选过 → 把目标写进会话选择(座位随之刷新)
   │     写之前先读走你的全局默认模型,写之后立刻还回去
   └─ 真正发送(宿主的请求水路再确认一次同样的目标)
```

三条性质因此同时成立:

1. **座位 = 本轮实际**:输入框里显示什么模型,这一轮就真的用什么模型;
2. **带图轮自动落到图片档**:计划本身就是 vision 目标,harness 的图片准入检查自然通过(不再需要单独的"带图自动切"开关);
3. **你手选的模型绝不会被覆盖**:预览返回 `manual` 标志时客户端原地不动,宿主也在同一轮跳过它,并在决策日志里记 `skip:manual`。

输入框下方那行读数带保留——它额外显示档位(simple/standard/hard/vision),与座位同源,不会再互相矛盾。

---

## 面板一览

**设置 → 插件 → 🧭 模型分流**

- **📡 最近一次实际请求**:档位 / 难度 / 领域 / 实际模型 / 你的输入(每 5 秒刷新,只读)
- **开关**:启用 / 尊重手动选择 / 记录日志 / 带图自动切模型 / 灰区判定
- **分档映射**:四个档位分别用哪个厂商、哪个模型、什么思考强度(留空=自动推荐)
- **图片档的思考强度**:简单 / 常规 / 困难 三档分别给 low / high / max
- **强制关键词**(可选,默认空):命中即强制某档,完全绕开打分
- **分类测试**:输入一句话 + 选配图张数(0/1/2),立刻看到判定与目标模型——不用真传图
- **最近决策**:时间 / 档位 / 难度·领域 / 判定依据 / 路由(`from → to`)/ 你的输入

### HTTP API(面板用的那几个,可脚本调用)

```bash
curl -s localhost:3080/api/model-router/state           # 配置 + 目录 + 上次决策 + 运行中代码指纹
curl -s -X POST localhost:3080/api/model-router/preview \
     -H 'content-type: application/json' \
     -d '{"text":"这个报错怎么解决","imageCount":1}'      # 只判定,不调用模型
curl -s localhost:3080/api/model-router/log?limit=30     # 最近决策
curl -s -X POST localhost:3080/api/model-router/config \
     -H 'content-type: application/json' -d '{"enabled":false}'   # 即时改配置
```

---

## 排查:这次改动到底要重启还是刷新?

插件分两半,规则完全不同:

| 改的地方 | 生效方式 |
|---|---|
| `lib/*.js`(host 半边:路由、分类、API) | **必须重启宿主**(Node 进程内存里的旧代码) |
| `lib/client.js`(client 半边:开关、面板、读数带) | **刷新浏览器页面**即可(服务端每次从磁盘读、`no-cache`) |
| `storages/dsh-effort-router/config.json`(档位、开关) | **都不用**,点面板即存即生效 |

自带一个自检脚本,让**运行中的宿主自己报出它加载的是哪份代码**:

```bash
node tools/check-reload.mjs                 # 默认检查 web profile 的安装目录
node tools/check-reload.mjs --src .         # 顺便比一下源码与安装副本是否一致
```

退出码:`0` 什么都不用做 · `1` 需要重启宿主 · `2` 安装目录不对 · `3` 连不上宿主 · `4` 源码还没装进 profile。

---

## 开发

```bash
npm run build     # 用 esbuild 把 src/client/index.jsx 打成 lib/client.js(host 半边不需要构建)
npm test          # 27 个用例:判定规则 / 图片阶梯 / 灰区判定 / 开关与手选 / 图片守卫 / 座位同步 / HTTP API
```

目录:

```
lib/                host 半边(Node):index.js 挂水路,router.js 分档,classify.js 规则,
                    judge.js 灰区小模型,config.js 配置,api.js HTTP 面板接口
src/client/         client 半边源码(React 设置面板 + 输入框开关 + 读数带)
lib/client.js       上面这份的构建产物(浏览器加载)
tools/check-reload  「要不要重启」自检
cordis.patch.yml    往宿主插件树插入自己那一行
```

---

## 已知限制

- **每轮只判一次**:判定发生在该轮第一个请求上,之后同轮不会重判。
- **只看本轮文本**:不读历史,所以"接着上面那个问题"这类指代型追问可能只拿到 standard——补一句带信号的说明即可。
- **带图自动切模型默认关**,原因见上文取舍表。
- 座位同步会让座位随每轮计划移动;若你希望座位固定在自己选的模型上,在座位上手动选一次即可——之后插件会让开(`skip:manual`)。

---

## English summary

**dsh-effort-router** is a [DeepSeek Harness](https://github.com/) plugin that routes **each request** (not the session) to the cheapest model that can still do the job:

- **Zero-token rule classifier** with four tiers — `simple / standard / hard / vision`.
- **Gray-zone judge**: only when the rules land on the ambiguous middle band, one 8-token call to a cheap model decides — no keyword whitelists to maintain.
- **Vision tier ladder**: one image model, effort scaled by difficulty (low/high/max), so a holiday photo and a stack-trace screenshot stop sharing one level.
- **The composer seat equals reality**: before every send the client applies the turn plan to the session selection — reading and restoring your global default model around it — so the model box shows what the turn will really use, and image turns reach the vision model without a separate guard. A model you picked by hand is never overridden (logged as `skip:manual`).
- **Instant switches**: an on/off button right in the composer, applied on the very next turn.

Install: `dsh plugin --profile web add dsh-effort-router-0.3.0.tgz`, then restart the host.
MIT licensed.

Install

dsh plugin --profile web add github:wanghaixu-hai/dsh-effort-router

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source