# dsh-plugin-thinking-api

<div align="center">

[English](./README.md) · **简体中文**

</div>

<p align="center">
  DeepSeek Harness 插件：一键配置任意 OpenAI 兼容 API 并自动带思考模式，同时规避 <code>developer</code> 角色被第三方端点拒绝（<code>content_filter</code>）的问题。
</p>

<p align="center">
  <img src="https://badgen.net/badge/license/MIT/blue" alt="license">
  <img src="https://badgen.net/badge/node/%3E%3D20/green" alt="node">
  <img src="https://badgen.net/badge/dsh/0.1--0-rc--6%20%E2%80%93%200.1--5-rc--1/purple" alt="dsh">
  <img src="https://github.com/qjf44/dsh-plugin-thinking-api/actions/workflows/check.yml/badge.svg" alt="ci">
</p>

---

## 解决什么问题

一个插件解决三件事：

1. **任意 OpenAI 兼容 API 一键配置** —— 腾讯 CodeBuddy、自建 vLLM、各类中转站，一个配置块搞定，无需手写 `reasoningEfforts`、`thinkingFormat` 或 pi-ai provider 内部结构。
2. **开箱即用的思考模式** —— 模型上写 `thinking: true` 就自动获得思考档位。
3. **修复 `content_filter` / `developer` 角色 bug** —— 这是最隐蔽的一个。

### 它修复的 bug

当模型声明了 reasoning（思考）时，pi-ai 会把 system prompt 改写成 OpenAI 的 `developer` 角色——除非它把该端点识别为「非标厂商」。不在 pi-ai 内置白名单里的第三方 API（腾讯 CodeBuddy 就是典型）因此会收到 `developer` 消息，而很多端点会硬拒绝它并返回 `content_filter`；同样的请求换成 `system` 角色则完全正常。

官方 `dsh-llm-pi-ai` 适配器在组装模型时**丢弃**了 `compat.supportsDeveloperRole` 字段，所以单靠 `settings.yaml` 无法修复。本插件自己组装 pi-ai 模型，直接注入 `compat.supportsDeveloperRole: false`，强制走 `system` 角色。

### 为什么它好维护

插件**复用**官方 `PiAiAdapter` 类（由 `@deepseek-ai/dsh-llm-pi-ai` 导出）：它的流式输出、chunk 翻译、凭据解析、空闲超时看门狗、图片处理都随 DSH 升级自动演进。本插件只负责「模型 / Provider 组装」这一小层——把正确的 `compat` 写进去。

## 安装

在 profile 的 `package.json` 里加入依赖和 bundle 列表：

```jsonc
// ~/.dsh/profiles/<profile>/package.json
{
  "dependencies": {
    "dsh-plugin-thinking-api": "github:qjf44/dsh-plugin-thinking-api"
  },
  "dsh": {
    "profile": {
      "bundles": [
        // ...你已有的 bundles...
        "dsh-plugin-thinking-api"
      ]
    }
  }
}
```

重装依赖并重启 Harness。插件的 `cordis.patch.yml` 会自动注册自身（`insert: [{ id: thinking-api }]`），**无需**手动改 `cordis.patch.yml`。

## 配置

在 `~/.dsh/settings.yaml` 里加一个 `thinking-api` 区块：

```yaml
thinking-api:
  providers:
    codebuddy:                                   # 路由 id（任意唯一名称）
      displayName: CodeBuddy                     # 可选，选择器里显示的名字
      baseURL: https://copilot.tencent.com/v2
      apiKeyEnv: CODEBUDDY_API_KEY               # 环境变量名；通过 Web 模型页存储或 export
      thinkingFormat: deepseek                   # 可选，默认 deepseek
      models:
        deepseek-v4-pro:
          name: DeepSeek V4 Pro
          thinking: true                         # ← 开启思考模式
        deepseek-v4-flash:
          name: DeepSeek V4 Flash
          thinking: false
        deepseek-v4.1-flash:
          name: DeepSeek V4.1 Flash
          thinking: true
          input: [text, image]                   # ← 声明可看图（多模态），read_image 才可用
```

存储密钥（**不要把明文 key 写进配置**）：

```bash
# 方式一：在启动环境里导出
export CODEBUDDY_API_KEY=ck_xxxxxxxx

# 方式二：通过 Web 界面 → 模型页写入（credentials 服务）
```

重启后，从模型选择器里选你的 API 模型即可。

> 📄 可直接复制的配置（CodeBuddy / 自建 vLLM / 任意 OpenAI 兼容中转站）：见 [`examples/settings.yaml`](./examples/settings.yaml)。

## Web 界面一键配置

插件还注册了 **设置 → 思考 API** 面板，内置一个统一的接入向导，不用手写 YAML 就能接入 API：

1. **来源** —— 选模板（CodeBuddy / DeepSeek / OpenRouter / …）或「自定义」。
2. **API 密钥** —— 粘贴一次即可。模板自带常用默认模型，填完 key 就能直接保存开始对话。
3. **模型**（可选）—— 点「获取模型列表」拉取端点模型，向导会按名称自动预判思考模型（每条都能用勾选框微调）；也可以手动添加模型 id。
   > 腾讯 CodeBuddy（`copilot.tencent.com`）不提供 OpenAI 兼容的 `/models` 端点，点「获取模型列表」时会自动填入模板预置模型（而非报 404 错误）；其他端点若 404 则需手动填写模型 id。
4. **保存** —— 完成。该 provider 会出现在模型选择器里，并走修复版适配器（`supportsDeveloperRole: false`）。

已接入的 provider 可以点「编辑」回填进向导修改，也可以删除。

> client 半通过 `exports["./client"]` 作为浏览器 bundle 被发现；修改插件源码后需重建 Web 产物，本 URL 才会加载新 bundle。

## 配置参考

### Provider（`providers.<id>` 下）

| 字段 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
| `baseURL` | string | ✅ | — | API 端点 base URL |
| `apiKeyEnv` | string | — | — | 存放 API key 的环境变量名 |
| `displayName` | string | — | 路由 id | 选择器里显示的名字 |
| `thinkingFormat` | enum | — | `deepseek` | `deepseek` \| `openai` \| `openrouter` \| `together` \| `zai` \| `qwen` \| `string-thinking` |
| `models` | dict | ✅ | — | 模型 id → 模型条目 |

### Model（`models.<id>` 下）

| 字段 | 类型 | 必填 | 默认 | 含义 |
|---|---|---|---|---|
| `name` | string | — | 模型 id | 显示名 |
| `thinking` | boolean | — | `false` | 是否开启思考档位 |
| `thinkingEfforts` | dict | — | 自动 | 自定义档位 → 线上参数映射，如 `{ off: null, high: high, max: xhigh }` |
| `contextWindow` | number | — | `262144` | 上下文窗口大小 |
| `maxTokens` | number | — | `32768` | 最大输出 token |
| `input` | array | — | `["text"]` | 输入模态：`["text"]` 或 `["text","image"]`。声明 `image` 后该模型才会被 DSH 认作视觉模型（`read_image` 才可用） |

`thinking: true` 且未给 `thinkingEfforts` 时，插件自动填充已验证可用的 DeepSeek 兼容档位（`off` / `high` / `max`，分别对应关闭 / `reasoning_effort: high` / `reasoning_effort: xhigh`）。需要更多档位（`low`/`medium`）或按 API 定制时，用 `thinkingEfforts` 显式覆盖。

> **关于 `input` / 图片输入**：DSH 的 `read_image` 会拒绝任何未声明 `image` 的路由（报 `model "..." does not declare image input`），而本插件默认把所有模型登记为纯文本。要让某个模型能看图，需显式声明 `input: [text, image]`，例如腾讯 CodeBuddy 的 `deepseek-v4.1-flash`（即官方原生多模态的 V4.1 Flash）。空数组与缺省同义，均回落为 `["text"]`。
>
> 注意这只是**声明**，不代表上游端点真的接受图片：声明后若端点拒收，会在真正传图时报错；反之若端点支持而你未声明，图片会在本地就被拦下、根本发不出去。

## 为什么不用内置 `llm-pi-ai`？

内置 `llm-pi-ai` 适配器本来就支持自定义 API，只是它无法表达 `supportsDeveloperRole`（它的 `compat` schema 没这个字段，解析器也会丢弃它），于是「白名单外 API + 思考模式」就会撞上 `developer` 角色拒绝。本插件存在的意义，就是补上这缺失的一个字段，其余全部复用。

## 支持的 DSH 版本

基于 DSH `0.1.0-rc.6` 至 `0.1.5-rc.1`（`@deepseek-ai/dsh-llm-pi-ai`）、pi-ai `^0.82.1` 构建。插件对 `PiAiAdapter` 的构造器形状做了防御性依赖；若未来 DSH 改变该内部契约，插件会给出清晰报错而非静默出错——升级 DSH 前请先看本插件的 release notes。

**DSH 0.1.5 兼容性。** 0.1.5 同时改动了多个内部契约，v0.1.2 及更早版本在其上会直接报错；v0.1.3–v0.1.5 依次修复（细节见 changelog）：

| 在 DSH 0.1.5 上的现象 | 根因 | 修复版本 |
| --- | --- | --- |
| 启动即 `Failed to load plugins` | `dsh.client.inject` 仍声明已被移除的 `@deepseek-ai/dsh-client-runtime` | v0.1.3 |
| 设置面板 `Cannot read properties of undefined (reading 'settings')` | `connection` 不再暴露 `.api`，取数须改走 `ctx.remote.*` | v0.1.4 |
| 模型选择器 `CodeBuddy 加载失败: Cannot read properties of undefined (reading 'get')` | 0.1.5 的 `pi-ai` `modelOf()` 会读 `profile.modelErrors`，而插件手组的 profile 缺该字段 | v0.1.5 |

## 升级 DSH 后 CodeBuddy / 第三方 API 用不了？先跑自检

插件依赖 pi-ai 与 DSH 的若干内部契约（provider auth 形状、`PiAiAdapter` 构造器、`llm` 服务注册方法等）。这些契约**不在官方语义版本保证内**，所以每次升级 DSH（或 pi-ai）后，如果模型突然报 `Provider is not configured`、`content_filter`、`MISSING_CREDENTIAL` 之类，先跑一次兼容性自检：

```bash
# 在插件仓库目录下（DSH workspace 会自动向上查找；找不到时显式指定）
node scripts/check-compat.mjs --workspace ~/.workbuddy/binaries/node/workspace
```

它会逐项核对：插件能否在真实依赖下 import、`PiAiAdapter` 构造器形状、pi-ai provider auth 形状、`llm` 服务注册方法、settings/credentials 辅助函数。**全部 ✓ 才能继续用；有任何 ✗ 就说明需要升级插件**（报错信息会点名是哪个契约变了、去哪改）。

### 历史踩坑记录（2026-09-12，图片输入「静默失效」）

用户反馈「CodeBuddy 的 `deepseek-v4.1-flash` 读不了图」，排查结论是**插件的问题，不是模型的问题**：

- **现象**：对任意图片调用 `read_image` 一律报 `model "<id>" does not declare image input; switch to an image-capable model to read images`。注意这个拒绝发生在**发出网络请求之前**。
- **根因**：`buildModel()` 把 `input` 硬编码为 `['text']`，而 `dsh-llm-pi-ai` 会把它原样映射为 `inputModalities`；`dsh-tool-fs` 的 `read_image` 据此拒绝任何未声明 `image` 的路由。于是图片**在本地就被丢掉**，上游是原生多模态模型也白搭。模型条目 schema 里当时根本没有 `input` 字段，用户无法从 `settings.yaml` 绕过。
- **修复**：新增可选 `input` 字段（默认 `['text']` 保持行为不变），并把 `buildModel` 改为读取它；空数组与缺省同义（对齐官方 `declaredInput` 语义）。

教训：**「模型不支持」和「路由没声明」是两回事。** 排查视觉问题时，先看本地能力声明（`inputModalities`），再怀疑上游端点——前者会把图拦在本地，后者的报错来自服务端，两者的报错位置和修复方式完全不同。

另一个值得记住的部署陷阱：profile 里的 `file:` 依赖是**拷贝**而非软链。只改插件仓库源码、不重跑 `pnpm install`，profile 里那份旧代码纹丝不动，改动静默不生效。

### 历史踩坑记录（2026-09-11，DSH 0.1.1-rc.2 → 0.1.5-rc.1）

0.1.5 一次挪动了三个契约，且各自只在**不同层面**才暴露——这正是「服务能启动」这类检查抓不到它们的原因：

- **客户端预加载（`Failed to load plugins`）。** 0.1.5 移除了 `@deepseek-ai/dsh-client-runtime`，但插件仍在 `dsh.client.inject` 里声明它，加载器找不到模块，整条插件图一起失败。`createSnapshotStore` 也迁到了内置 seed 模块 `@deepseek-ai/dsh-client-store`。修复：从 inject 去掉 runtime，改从 seed 模块导入 store。
- **渲染层取数（`reading 'settings'`）。** `connection` 不再携带 `.api`，settings/credentials/llm 的读取须改走 cordis 命名空间服务 `ctx.remote.settings` / `ctx.remote.credentials` / `ctx.remote.llm`。注意客户端 bundle 是按内容哈希按请求下发的，这一半**刷新页面即生效，无需重启宿主**。
- **宿主 profile 形状（`reading 'get'`）。** 0.1.5 的 `pi-ai` `modelOf()` 会无条件执行 `profile.modelErrors.get(model)`。插件是手工组 profile 的（无法直接复用官方 `resolveProfiles`——它没有 `userAgent` / `compat.supportsDeveloperRole` 这两个钩子），而这个手组对象早于该字段存在，于是 `modelCatalog` 枚举时整个 provider 组抛错。单看 `listModels` 走的是 `getModels()`、本身安全，所以该组**能列出**、只在选择器解析每个模型详情时才失败。修复：补上 `modelErrors: new Map()` 以及官方 profile 同样携带的图片预算默认值。**这一半是宿主代码，必须重启 harness 才生效。**

教训：DSH 升级后要在三个层面分别验证（启动、设置面板、模型选择器），不能只看服务有没有起来。

### 历史踩坑记录（2026-08-18，DSH rc.6 → rc.7）

升级后 codebuddy 请求 100% 报 `PI_AI_ERROR: Provider is not configured: codebuddy`，根因是 **pi-ai 0.82.1 改了 provider auth 契约**：

- 旧（0.82.1 之前）：`auth: { name, resolve }`（顶层 `resolve`）。
- 新（0.82.1 起）：`resolveProviderAuth` 只认 `auth.apiKey.resolve`，顶层 `resolve` 被当作「无认证方式」→ `getAuth` 返回空 → 上述报错。

修复：`buildProvider` 把 auth 组装成 `{ apiKey: { name, resolve } }`，与官方 `dsh-llm-pi-ai` 的 `routeAuth`/`harnessApiKeyAuth` 形态一致。插件现在启动时会自检该契约，版本不匹配会在启动时直接报错，而不是等你发消息。

另一个坑：**GUI 向导编辑 provider 时 key 留空会抹掉已有的 `apiKeyEnv`**，导致升级/重配后密钥引用丢失（报 `MISSING_CREDENTIAL` 或认证失效）。已修复：编辑模式 key 留空表示「不改」，沿用原有 `apiKeyEnv`。

## 参与贡献

欢迎提交 bug 和反馈——请用 [bug 报告模板](https://github.com/qjf44/dsh-plugin-thinking-api/issues/new/choose) 建 issue（模板会要 DSH 版本、插件版本和 check-compat 输出，能覆盖九成故障）。

提 PR 前先跑本地检查：

```bash
npm run check                 # 语法（lib/index.mjs、lib/client.js、scripts/check-compat.mjs）
node scripts/check-compat.mjs --workspace ~/.workbuddy/binaries/node/workspace   # 对真实 DSH 做契约自检
```

每次 push/PR 时 CI 会跑 `npm run check` 加一个 npm 包体检 job（tarball 文件清单、版本号是否已发布）——见 [`.github/workflows/check.yml`](.github/workflows/check.yml)。

## License

[MIT](./LICENSE)
