本地模型路由器(dsh-local-model-router)
决定 DSH 什么时候把请求交给本地配置的模型,并且真的把它切过去。
不是"再写一个适配器",而是一个路由插件:它挂在两处官方扩展点上,按策略改写这次调用的目标模型。
| 请求类型 | 识别方式 | 默认行为 |
|---|
后台杂活(上下文压缩、会话标题、其它带 purpose 的调用) | llm/stream 洪水式事件上的 options.purpose | 走本地模型 |
| 主对话每一轮 | agent/request 事件 | 走云端模型,除非 /local main |
English: a DSH (DeepSeek Harness) plugin that decides when an LLM call is
served by your locally configured model — session titles and other background
calls on local by default, the main conversation only via /local main.
The rest of this document is written in Chinese.
安装
本插件是 Host-only bundle(零依赖、无需构建)。把它装进当前 DSH profile:
plugin_manager(action="install_bundle", target="<本目录的绝对路径>")
装完后插件行出现在 cordis.patch.yml 里(include:local-model-router),
list_plugins 能看到 fiberPhase: "active"。升级同一个包需要先
remove_bundle 再 install_bundle,且新的 JS 世代要重启应用才载入。
前置条件
-
本地 runtime 里要有模型。默认目标是 Ollama 的 qwen3:
ollama pull qwen3
curl http://127.0.0.1:11434/api/tags # 确认能看到 qwen3
-
它必须是 DSH 已注册的 provider route。插件不改写 provider 目录,只在
listProviders() 里已经存在 provider 时才路由。在「设置 → 模型/供应商」里加一条
OpenAI 兼容渠道即可:
| 字段 | 值 |
|---|
| route id | ollama |
| baseURL | http://127.0.0.1:11434/v1 |
| api | openai-completions |
| model id | qwen3 |
| apiKey | 随便填(Ollama 不校验) |
路由名必须和插件配置里的 provider 一致;万一你起成 ollama-local 之类,插件会在
listProviders() 里找不到 ollama 时自动挑一个名字像 Ollama/LM Studio 的 route 顶上。
装好后运行 /local status,它会直接告诉你 ollama 是否已注册、端点是否可达、qwen3
是否已 pull、以及思考档位是否关掉了。
命令
| 命令 | 作用 |
|---|
/local 或 /local on | 本会话背景任务 → 本地模型 |
/local off | 本会话背景任务 → 调用方原本的模型 |
/local main | 切换本会话主对话是否走本地模型(再次输入即关闭) |
/local status | 诊断:目标模型、provider 目录、本会话覆盖、探活、路由/回退统计 |
/local reset | 清掉本会话覆盖 |
/local ollama/qwen3 | 本次运行临时换目标(不落盘) |
/auto | 清掉本会话覆盖,回到默认策略 |
在当前的 desktop profile 里实际会发生什么
已核对宿主内官方源码,不是猜的:
- 会话标题会被路由。
@deepseek-ai/dsh-session-title-llm 实际构造的是
deepFreeze({ ...call.config, messages, system, sessionId, purpose: "session-title", signal }),
带 purpose 也带 sessionId —— 所以本插件读得到、且会话级 /local 覆盖对标题也生效。
- 上下文压缩不会被路由,因为这个 profile 里它根本不发生第二次 LLM 调用:
billion-context 的 bili-compaction-gate 会把官方 @deepseek-ai/dsh-compaction-basic 和
@deepseek-ai/dsh-command-compact 两行直接改成 disabled: true(list_plugins 里确实都是
enabled: false),压缩改由 Agent 自己调 compress 工具写摘要 —— 那是主对话的一部分。
哪天你把机制换回官方自动压缩,purpose: "compaction" 就会自动进入路由范围。
- 所以默认配置的实际收益 = 标题走本地;想要"整段对话走本地",用
/local main。
行为细节
- 失败回退:适配器错误不会抛异常,而是以
finish + reason.kind === 'error' 结束流。
插件在"一个内容块都没吐"的情况下捕获它,改用调用方原本的模型重放这次请求;已经吐了内容
就直接透传,不重放。
- 熔断:连续 2 次本地失败后,后台路由冷却 60 秒,期间不打扰本地端点。
- 思考模型要关思考(
disableThinking: true,默认开)。qwen3 这类模型先吐 reasoning
再吐正文;会话标题的输出预算很小,思考会把预算吃光,finish_reason 变成 length、
正文为空 —— 标题就白跑一趟。Ollama 的 OpenAI 兼容端点里 "reasoning_effort": "none"
可以关掉思考(实测 0.40.6 生效),所以插件会先问一下目标模型的档位表
(ctx.llm.resolveModelInfo),若有 none/off/disabled 档就带上它;没有就退回
"不传档位",此时标题若因推理占满预算而失败,照常有失败回退兜底(走云端)。
- 不改冻结请求:agent-loop 构造的请求是 deep-frozen 的,插件只读不写;改写永远是派生新
对象(并去掉调用方的
reasoningEffort,因为本地模型通常不声明这套档位,显式档位会在 I/O
前被拒)。
- 重入守卫:插件自己发起的重入用
AsyncLocalStorage 标记,不会二次路由;重入调用放在
generator 首次 pull 时才创建,因此落在外层包装(如 dsh-cost-meter)的异步上下文里,不会被
重复计费。
- 会话隔离:
/local 只影响当前会话;子代理有自己的 agent id,不继承。
- 计费口径:路由后的调用在成本面板里仍按调用方声明的模型记账(外层包装先于本插件注册),
后台杂活的量很小,一般可忽略。
配置项
安装后行配置在插件行里可改(cordis.patch.yml 的 config):
enabled: true # 总开关
provider: ollama # 本地 provider route
model: qwen3
purposes: [compaction, session-title] # 默认接管的 purpose
mainTurn: false # 主对话是否默认走本地(不建议,见下)
fallbackOnError: true
disableThinking: true # 有"关闭思考"档位时带上它(标题类小预算调用必需)
probeUrl: http://127.0.0.1:11434/api/tags
verbose: true # 路由决策打到宿主日志
关于主对话
mainTurn / /local main 是显式实验开关。DSH 的工具调用协议对模型有要求,本地小模型
的工具调用可靠性差异很大;主对话切过去后请自己观察,出问题就 /local main 关掉或 /auto。
自测
cd dsh-local-router
node test/smoke.mjs # 12 项:路由、回退、会话隔离、命令、冻结对象不变、思考档位、route 自动识别、模型名归一
测试只用一个最小的 fake host(自己拼的 ctx.llm / ctx.commands / ctx.on),
不需要跑 DSH 本体,也不联网。
许可
MIT。
免责
这是个改路由的插件:它会真的把你的请求发给本地端点。主对话开关(mainTurn /
/local main)属于实验性质,本地小模型的工具调用可靠性差异很大,请自行观察效果。