# dsh-model-reasoning-levels DeepSeek Harness **Settings → Models** 里按模型设置的推理强度等级。 [English](README.md) | **简体中文** [![License](https://img.shields.io/github/license/icrefin/dsh-model-reasoning-levels?style=flat-square)](LICENSE) [![Release](https://img.shields.io/github/v/release/icrefin/dsh-model-reasoning-levels?style=flat-square)](https://github.com/icrefin/dsh-model-reasoning-levels/releases/tag/v0.1.2) [![CI](https://img.shields.io/github/actions/workflow/status/icrefin/dsh-model-reasoning-levels/ci.yml?branch=main&style=flat-square)](https://github.com/icrefin/dsh-model-reasoning-levels/actions/workflows/ci.yml) [![Last commit](https://img.shields.io/github/last-commit/icrefin/dsh-model-reasoning-levels?style=flat-square)](https://github.com/icrefin/dsh-model-reasoning-levels/commits/main) [![Stars](https://img.shields.io/github/stars/icrefin/dsh-model-reasoning-levels?style=flat-square)](https://github.com/icrefin/dsh-model-reasoning-levels/stargazers) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square)](https://github.com/icrefin/dsh-model-reasoning-levels/pulls) [![Node](https://img.shields.io/badge/node-%3E%3D22.17-339933?style=flat-square&logo=node.js)](https://nodejs.org) [![dsh](https://img.shields.io/badge/dsh-0.2.0--rc.2-4B5563?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness) **Model Reasoning Levels** —— 补齐每个模型缺失的推理强度控件,直接写进该模型在 profile `cordis.patch.yml` 中的 `reasoningEfforts`。 随包发布的 Models 页面可以编辑模型的 `id`、显示名、上下文窗口、最大输出 token 和输入类型,却**有意不编辑**推理强度:推理强度是**按模型**的能力, 而那个编辑器是**按提供商**的。本插件把缺失的控件加在**每个模型条目内部** ——就在 id/名字那一行的下方,或者展开后的上下文窗口/输入类型区域下方—— 并把选中的内容转换成该模型在 profile `cordis.patch.yml` 里的 `reasoningEfforts`。 ## 它贡献了什么 | 界面 | 作用 | |---|---| | `settings.models.provider-card`(key 为 `llm-pi-ai`) | 抵达每个 pi-ai 提供商卡片,发现它的各个模型行,并把一份按模型设置的强度编辑器 portal 进每个模型条目——手工声明的路由同样覆盖 | | Locale 命名空间 `dsh-model-reasoning-levels` | 英文与简体中文文案 | | `scripts/reasoning-efforts.mjs` | 校验一份选择,并打印它变成的 `reasoningEfforts` YAML 与 settings 路径 op | 只有浏览器这一半:宿主半(`exports["."]`)的存在,只是为了让 Loader 行能带上 浏览器半(`exports["./client"]`)。没有路由、没有 service、没有 Config。 ## 它长什么样 ``` Settings → Models → Baidu Team → Customized settings → Models ┌─────────────────────────────────────────────────────────────┐ │ [deepseek-v4.1-flash] [DeepSeek-V4.1-Flash] ⌄ 🗑 │ │ Reasoning effort levels Inherited │ │ [Off] [Minimal] [Low] [Medium] [High] [XHigh] [Max] │ │ Low wire value: low High wire value: high │ │ [Inherit catalog] [Non-reasoning] [Apply] { off: null, … } │ └─────────────────────────────────────────────────────────────┘ ``` ![Settings → Models 页面里附加在模型条目内部的推理强度等级控件](docs/images/reasoning-levels-row.png) *控件就长在一个模型条目里:七枚等级芯片、每个已选的思考等级一个协议值输入框、 **Inherit catalog** / **Non-reasoning** / **Apply** 三个动作,以及当前选择会变成的 那个确切的 `reasoningEfforts` 值。* ## 控件如何进入模型行 随包发布的页面**没有在模型行内部声明任何 seat**,它的 `ModelRow` 是内部组件。 唯一被认可的、行级的改动方式,是整体遮蔽 `settings.section` 的 `models` 条目 ——也就是把整个页面整体遮蔽一份。本插件走的是更窄的那条路:先拿到提供商卡片的 seat,再以结构化方式发现每一个模型行。 - 模型行的 id 输入框带着由随包页面的 `settings.models` locale 命名空间生成的 `aria-label`(`Model ID 1`、`模型 ID 1`……)。插件绑定该命名空间并匹配这个 label,因此发现过程跟随当前语言,也从不依赖被哈希过的 CSS 类名。 - 它为每个 `modelEntry` 追加一个容器,并把 React 编辑器 portal 进去,所以控件 就活在模型条目内部,也随该行一起消失。 - `MutationObserver` 在折叠区展开、增删一行或页面重渲染时重新发现。随包页面 若调整了行的结构,这个选择器就需要跟着更新。 ### 保存,以及页面自身的 Apply 编辑器通过 settings 传输层写入(`settings.mutate` 路径 op),由宿主负责持久化。 随包页面持有它**自己的一份模型列表草稿**,它自己的 Apply 会为手工声明的路由 整体写入 `models` 数组,这可能把卡片打开时载入的 `reasoningEfforts` 值又还原 回去。 为了让编辑器里的值说话算数,这一行会记住用户应用过的声明;如果后续的设置变更 把它改回去,就再写一次——最多三次,这样被拒绝时会如实报告,而不是无休止重试。 因此先应用强度、再用卡片的 Apply 改别的字段,最终会收敛到你选中的强度值。 对目录(catalog)路由,编辑器走的是更窄的 `providers..modelOverrides..reasoningEfforts` 路径,页面的模型编辑器 不会碰它——除非页面(通过编辑模型列表)把 `models` 数组实体化,而那种情况下 适配器会拒绝 `modelOverrides` 与它并存。只要卡片还开着,这一行的重写就能覆盖 这种情况。 ## 等级词汇表是封闭且有序的 `off, minimal, low, medium, high, xhigh, max`——正是 harness 自己的 `THINKING_LEVELS`,按强度递增排列。无论芯片以什么顺序点击,编辑器始终按这个 顺序生成映射。`off` 表示“不思考”,其余六个是思考等级。 ## 转换规则(用的是适配器自己的规则) 每个被选中的等级都会成为一个 key,其值是 dispatch 发出的那个拼写,所以自带 词汇表的网关可以改名(`max: ultra`)。插件在写入之前就强制执行 `@deepseek-ai/dsh-llm-pi-ai` 的 `resolveModelReasoning` 规则: | 选择 | 结果 | |---|---| | 带协议值的 `off, low, high, max` | `{ off: null, low: 'low', high: 'high', max: 'max' }` | | 只有 `off` | 拒绝——不能思考的模型用 `false` | | 什么都没选 | 拒绝——要继承就省略这个字段 | | 非 `off` 等级缺协议值 | 拒绝——它需要 dispatch 发出的那个值 | | 协议值为空字符串 | 拒绝 | | 七个之外的等级 | 拒绝 | | “Non-reasoning” 形式 | `reasoningEfforts: false` | | “Inherit catalog” 形式 | 该字段被 unset,恢复继承 | 一份声明出来的映射会**替换**模型的能力集合:没声明进去的等级就不会被提供。 所以用户保留 `off` 时它必须出现在映射里,也所以“只有 off”是 `false`,而不是 `{ off: null }`。 ## 值最终落在 `cordis.patch.yml` 的哪里 编辑器写的是 settings 路径 op,宿主把它们持久化进 profile 的 `cordis.patch.yml`。`reasoningEfforts` 与随包行本来就在编辑的那些字段并列: ```yaml - id: llm-pi-ai name: "@deepseek-ai/dsh-llm-pi-ai" config: providers: baidu-team: models: - id: deepseek-v4.1-flash reasoningEfforts: { off: null, low: 'low', high: 'high', max: 'max' } contextWindow: 1000000 maxTokens: 256000 input: [text, image] name: DeepSeek-V4.1-Flash ``` 展开写法和上面等价,手工编辑时更省事: ```yaml reasoningEfforts: off: low: 'low' high: 'high' max: 'max' ``` 一个不能思考的模型,和一个保留已安装目录能力的模型: ```yaml - id: acme-plain reasoningEfforts: false # no selector beyond off - id: acme-catalog contextWindow: 128000 # reasoningEfforts omitted -> inherits ``` ### 两个落点,以及编辑器为什么在两者之间选择 `llm-pi-ai` 承载一个模型条目时,用的是两种互斥形状之一: | 落点 | 形状 | 编辑器的写入 | |---|---|---| | `providers..models[]` | 列表,条目自带 `id` | 按值替换:**整个数组** | | `providers..modelOverrides.` | 以模型 id 为键的字典 | 一条窄路径:只写 `reasoningEfforts` | 适配器会拒绝:`modelOverrides` 与 `models` 列表并存、落在手工声明的路由上、 以及针对已安装目录并不描述的 id。所以路由配置了 `models`(或是手工声明)时, 编辑器用 `models`;目录描述了该路由且不存在 `models` 列表时,用最小的 `modelOverrides` 路径。走 `models` 时会实体化完整数组,这样一个条目的编辑 不会删掉目录里的其余条目;而窄路径的 `modelOverrides` 写入不会在该条目上 钉住任何别的字段。 编辑器读这个列表的方式与页面一致——用户层拥有 `models` 时读用户层,否则读 组合基底,再否则读 schema 默认值——当两者都没有声明列表时,通过 `llm/discoverModels` 载入已安装目录(对目录提供商没有 endpoint I/O)。它 从不把解析后的 `value` 引用进任何一次写入。 ## 环境要求 - **DeepSeek Harness `0.2.0-rc.2`**——插件在 `engines.dsh` 里声明,并把 `@deepseek-ai/dsh-*` 的 peer 依赖固定在这一条线上。 - 从源码构建需要 **Node.js >= 22.17** 和 **pnpm 11.8.0**。 - 一个可写的 profile。桌面应用上的安装入口是 **Settings → Plugins**(见[安装](#安装))。 ## 安装 本仓库只发布 TypeScript 源码——`lib/` 被 gitignore——所以安装 bundle 之前先构建它: ```sh git clone https://github.com/icrefin/dsh-model-reasoning-levels.git cd dsh-model-reasoning-levels pnpm install pnpm build # tsc -> lib/, tsdown -> lib/client.js sh scripts/install.sh # packs dist/ and prints the install path sh scripts/install.sh --profile # or pack and install into that profile ``` `scripts/install.sh` 会打包出 `dist/dsh-model-reasoning-levels-.tgz`,并 打印接下来怎么用它。加上 `--profile ` 则改为把 tarball 安装进一个 CLI 管理的 profile,而不是只打印指引。 桌面 profile 归 Electron 应用所有,所以请从 **Settings → Plugins** 安装这个 tarball(插件管理器工具也可以);`dsh plugin add` 会拒绝该 profile。往正在运行的 profile 里加 bundle 需要**重启**;客户端这一半还需要刷新页面。 开发用的检查套件是同一批命令: ```sh pnpm check # typecheck both halves, build, then vitest pnpm yaml check --levels off,low,high,max --wire low=low,high=high,max=ultra ``` `pnpm check` 先分别对宿主半与浏览器半做类型检查,再构建 bundle,最后跑测试套件 ——**5 个文件共 50 个测试**。 ## 使用 1. 打开 **Settings → Models**,展开一个 pi-ai 提供商卡片(例如手工声明的 `baidu-team` 路由,或 OpenAI 这样的目录路由)。 2. 在 **Customized settings → Models** 下,每个模型行现在都带着自己的 **Reasoning effort levels** 控件。选中该模型对外提供的等级;每个非 `off` 等级都有一个协议值输入框,默认填的就是等级 id。 3. 用 **Inherit catalog** 省略 `reasoningEfforts`,模型不能思考时用 **Non-reasoning**。 4. **Apply**。控件写入一次 settings 变更并重新读取;旁边的小代码块显示确切的 YAML 值。 `llm-deepseek` 路由没有按模型的字段——它们的强度是路由级的——所以这个编辑器 只为 `llm-pi-ai` 适配器家族渲染。 ### CLI:预览 YAML 与对应的 op ```sh node scripts/reasoning-efforts.mjs check --levels off,low,high,max --wire low=low,high=high,max=ultra node scripts/reasoning-efforts.mjs yaml --route baidu-team --model deepseek-v4.1-flash \ --levels off,low,high,max --wire low=low,high=high,max=max node scripts/reasoning-efforts.mjs ops --route acme --model acme-think --overrides \ --levels off,high --wire high=high node scripts/reasoning-efforts.mjs none --model acme-plain ``` `pnpm yaml` 就是同一个脚本。`--json` 输出机器可读的结果。退出码 1 表示这份选择 不合法,原因写到 stderr。 ## 限制 - **只支持 `llm-pi-ai` 适配器家族。** `llm-deepseek` 路由没有按模型的强度字段, 编辑器对它们不渲染任何东西。 - **行发现是结构化的。** 它跟随随包页面自己的本地化 `Model ID ` label;随包 页面若重排了行结构,这个选择器就要更新,`MutationObserver` 重新发现的也正是 这种情况。 - **插件自己不写 YAML。** 它只发送 settings 路径 op,由宿主持久化。settings 文档只读时控件保持禁用;被拒绝或版本冲突的写入会在该行里报出来,而不是反复 重试。 - **重写有次数上限。** 已应用的值最多被还原三次,之后如实报告冲突。 - **目录路由依赖宿主的目录。** 没有配置 `models` 列表的路由,其模型行来自 `llm/discoverModels`;拿不到它时,只有声明了 `models` 列表的路由可编辑。 ## 常见问题 **我应用的值,被页面自己的 Apply 弄回去了。** 随包页面持有它自己的一份模型列表草稿,对手工声明的路由,它的 Apply 会整体写入 `models` 数组——把卡片打开时载入的 `reasoningEfforts` 值还原回来。这一行会察觉 并把你的值再写一次,最多三次;如果它报了冲突,重新加载卡片再应用一次即可。 **模型以前提供的某个等级,现在从选择器里消失了。** 声明出来的映射就是模型完整的能力集合:映射里没列出的等级不会被提供。在这里把该 等级加回来,或者选 **Inherit catalog** 丢掉这份声明,保留已安装目录条目的能力。 **为什么单独一个 `off` 会被拒绝?** 只能“不思考”的模型就是非推理模型,适配器把这种情况拼作 `reasoningEfforts: false`, 请用 **Non-reasoning**。在映射内部,`off` 的含义是“被要求这个等级时不发送任何 内容”,所以它至少要和一个思考等级并列。 **某个模型行里没有出现这个控件。** 先看路由:只有 `llm-pi-ai` 卡片会拿到它。如果该行的 id 输入框是空的,先保存这个 模型——没有模型 id 的编辑器无从指定对象,它会这么告诉你。 ## 许可证 MIT——见 [LICENSE](./LICENSE)。 > 如果这个插件让你少改一次 YAML patch,一颗 ⭐ star 能帮更多 DSH 用户发现它。 DeepSeek Harness 的独立插件——与 DeepSeek 官方无关。