# dsh-llm-openai-compatible(万能插头) 让 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 接上**任意 OpenAI 兼容端点**:本地 vLLM / LM Studio / llama.cpp 服务器、Ollama 的兼容层、或任何远程网关(OpenRouter、Together、Moonshot 等)。**装完 + 配好端点就能跑**——不需要装 Ollama,也不需要改 dsh 核心。 > 设计目标:这个插件自己就是一个「万能插头」——一个 provider 路由(`openai-compatible`),任何说 OpenAI Chat Completions 协议的服务都能插上来。 ## 特性 - **任意 OpenAI 兼容端点**:`baseURL` 配 `http://127.0.0.1:8000` 或 `http://127.0.0.1:8000/v1` 都行(自动归一化到 `/v1`)。 - **API key 可选**:本地服务通常不需要鉴权——没配 key 时请求匿名发出(本地端点忽略多余 Bearer 头);远程网关必须配 key,否则 401。 - **模型目录**:`models` 数组声明端点实际提供的模型(id / contextWindow / maxTokens / vision / thinking / defaultEffort)。 - **Web 配置**:Settings → Plugins → `llm-openai-compatible`,通用表单即可改端点、key、模型目录、重试策略,保存即时生效。 - **模型发现**:`discoverModels()` 读 `GET /v1/models`,返回端点真实提供的模型 id 列表。 - **免 allowlist 安装**:仓库提交构建产物 `lib/`(无 `prepare` 脚本),GitHub 安装不需要 pnpm 的 build-script 白名单。 ## 安装 要求 DeepSeek Harness 0.1.0-rc.6+。 ```sh # 从 GitHub 安装(推荐,免本地构建) dsh plugin --profile web add github:cqnxnzg/dsh-llm-openai-compatible # 本地开发安装(<仓库路径> 替换为克隆下来的插件目录;先 pnpm run build) dsh plugin --profile web add <仓库路径>/dsh-llm-openai-compatible dsh web ``` > GitHub 安装无需 build-script allowlist:仓库提交了构建产物 `lib/`(无 `prepare` 脚本),装完即可用。本地开发时改源码后记得 `pnpm run build` 再重装。 ## 配置 ### 最小配置(本地 vLLM 等) 默认 `baseURL = http://127.0.0.1:8000/v1`,默认模型目录里有几个常见本地模型 id。打开 **Settings → Plugins → llm-openai-compatible**,把 `models[].id` 改成你本地服务**实际提供**的模型 id(见下文「UNKNOWN_MODEL 怎么消除」),保存即可在模型选择器里选中聊天。 ### 配置字段(全部可选) | 字段 | 默认 | 说明 | |---|---|---| | `apiKeyEnv` | `OPENAI_API_KEY` | 凭据引用(环境变量名);未配置/为空 → 匿名请求(本地端点可用) | | `baseURL` | `http://127.0.0.1:8000/v1` | OpenAI 兼容端点;自动归一化到 `/v1` | | `models` | 4 个示例模型 | 端点实际服务的模型目录;未列出则请求报 `UNKNOWN_MODEL` | | `maxTokens` | — | **全局**默认输出上限;模型行未声明时兜底 | | `defaultContextWindow` | `131072` | 模型未声明 contextWindow 时的上下文容量 | | `streamIdleTimeoutMs` | `300000` | 流式读取空闲超时 | | `retryPolicy` | 正常默认 | 重试策略(见下) | **`models[].*` 字段语义:** | 字段 | 说明 | |---|---| | `id` | 端点接受的模型 id(**必须**与端点实际服务的一致,否则 `UNKNOWN_MODEL`) | | `name` | 选择器显示名;省略用 `id` | | `description` | 选择器里的补充说明(可选) | | `contextWindow` | 该模型上下文容量(token) | | `maxTokens` | **该模型专属**输出上限,优先于全局 `maxTokens`;请求级 `maxTokens` 又优先于它 | | `vision` | `true` = 接受图片输入(请求带图时输入模态含 image) | | `thinking` | `true` = 支持原生思考;选择器可调 thinking 等级(off/low/medium/high/max) | | `defaultEffort` | 聊天选择器的默认思考等级;需 `thinking: true` 且等级在支持集合内才生效 | | `tools` | 遗留能力标志,运行时忽略,仍被解码 | **`retryPolicy` 可配置值**(省略 = 正常默认:最多重试 2 次,重试码 `EMPTY_RESPONSE`/`RATE_LIMIT`/`SERVER`/`TIMEOUT`/`TRANSPORT`,退避 `initialDelayMs: 500` / `maxDelayMs: 10000` / `jitterRatio: 0.1`): ```yaml retryPolicy: mode: normal # normal | always maxRetries: 3 # normal 模式:最大重试次数 retryableCodes: [RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT] # normal 模式:可重试错误码 backoff: initialDelayMs: 500 maxDelayMs: 10000 jitterRatio: 0.1 # mode: always = 无条件重试(只有 backoff 字段),一般用于本地服务 ``` ### 常见本地端点 | 服务 | baseURL | 备注 | |---|---|---| | vLLM | `http://127.0.0.1:8000/v1` | 默认值;`--served-model-name` 指定的名字才是请求 id | | LM Studio | `http://127.0.0.1:1234/v1` | — | | llama.cpp server | `http://127.0.0.1:8080/v1` | — | | Ollama(兼容层) | `http://127.0.0.1:11434/v1` | Ollama 原生 API 不是 OpenAI 协议;**必须走它的 `/v1` 兼容层**,模型 id 常带 tag,如 `qwen2.5:7b` | | OpenRouter | `https://openrouter.ai/api/v1` | 网关,**必须配 `apiKeyEnv`**,否则 401 | | Together | `https://api.together.xyz/v1` | 网关,**必须配 `apiKeyEnv`**,否则 401 | | 其他远程网关 | `https://.../v1` | 网关通常需要 key;见故障排查 | ### 接 DeepSeek 官方 API 完整示例 DeepSeek 官方端点本身就是 OpenAI 兼容的。在 Settings → Plugins → `llm-openai-compatible`(或 settings 文档的 `llm-openai-compatible` 节)里: ```yaml llm-openai-compatible: apiKeyEnv: DEEPSEEK_API_KEY # 环境变量里放你的 DeepSeek API key baseURL: https://api.deepseek.com # 自动路由到 /v1,不用手写 /v1 models: - id: deepseek-chat # DeepSeek-V3,通用对话 name: DeepSeek Chat contextWindow: 65536 maxTokens: 8192 tools: true - id: deepseek-reasoner # DeepSeek-R1,推理模型,需要 thinking name: DeepSeek Reasoner contextWindow: 65536 maxTokens: 8192 thinking: true ``` 要点: - `baseURL: https://api.deepseek.com` 即可——插件会自动补 `/v1`(等价于写 `https://api.deepseek.com/v1`)。 - `deepseek-reasoner` 是推理模型,目录行要标 `thinking: true`,这样选择器才能正确展示思考等级。 - 环境变量 `DEEPSEEK_API_KEY` 由 dsh 的凭据缝读取(`apiKeyEnv` 指的就是环境变量名);配好 key 后请求带 `Bearer` 鉴权,不会 401。 ## 无真实模型也能测(装完后的第一课) ```sh node scripts/mock-server.mjs # 起一个 OpenAI 兼容 mock(GET /v1/models + POST /v1/chat/completions) pnpm run smoke # 用构建产物跑一次 adapter 全链路,打印模型列表 + 流式回复 pnpm run verify # 系统性自验证:52 项断言(纯函数 / schema / adapter / discovery) ``` `smoke` 打印 `SMOKE OK`、`verify` 打印 `VERIFY OK` 且退出码 0 = 万能插头端到端打通。 ### 真实 dsh profile 端到端验证 已用一个独立 profile(`plugtest`)验证过完整链路:dsh-base + dsh-headless + 本插件,patch 层把 `agent-default-model` 指向 `openai-compatible/gpt-oss-120b`、插件 `baseURL` 指向本地 mock, 然后一次 headless 任务直接拿到 mock 的回复: ```sh dsh --profile plugtest "你好,请用一句话自我介绍" # [mock:gpt-oss-120b] 你好,万能插头已接通!Hello from the OpenAI-compatible mock. auth=Bearer no-key-local ``` 验证要点:插件在真实 profile 中加载、provider/adapter/discovery 注册、agent loop 走通、 settings 指向 profile 专属文件(不触碰全局 `~/.dsh/settings.yaml`)。 ## UNKNOWN_MODEL 怎么消除 `UNKNOWN_MODEL` 表示请求的模型 id 不在插件的 `models` 目录里。按下面三步解决: 1. **问端点要真实 id**: ```sh curl http://127.0.0.1:8000/v1/models # {"object":"list","data":[{"id":"Qwen/Qwen2.5-7B-Instruct",...}, ...]} ``` 把 `data[].id` 原样抄进 `models[].id`(插件也提供 `discoverModels()` 做这件事,Web 配置的「fetch models」动作可一键导入)。 2. **vLLM 特殊**:启动参数 `--served-model-name` 决定请求 id,可能与你下载的模型名不同(比如下载的是 `Qwen2.5-7B-Instruct`,服务名却是 `qwen-7b`)。以 `curl /v1/models` 返回的为准。 3. **Ollama 特殊**:兼容层返回的 id 常带 tag(如 `qwen2.5:7b`),照抄,别去掉 `:7b`。 ## 故障排查 | 症状 | 原因与解法 | |---|---| | `UNKNOWN_MODEL` | 模型 id 不在 `models` 目录;按上文「UNKNOWN_MODEL 怎么消除」抄真实 id | | `401 Unauthorized` | 端点需要 key:`apiKeyEnv` 配了没?环境变量值对吗?本地端点不需要 key 时把 key 清空(匿名请求) | | 404 / 连不上 | `baseURL` 写成了完整路径(如 `.../v1/chat/completions`)——只要 base,插件自动补 `/v1`;或服务没起 / 端口不对 | | `/v1` 写两遍 | `baseURL` 写 `http://host:8000/v1` 或 `http://host:8000` 都行,**不要**写 `http://host:8000/v1/v1` | | 模型选择器里没有我的模型 | `models[].id` 与端点返回不一致;或保存后没等配置生效(保存即生效,重开选择器刷新) | | 匿名请求被本地端点拒绝 | 个别本地服务校验 Bearer 头;给它配一个任意 key(`apiKeyEnv` 指向一个假值)试试 | **诊断命令**(从插件目录跑): ```sh curl http://127.0.0.1:8000/v1/models # 端点到底有哪些模型 id node scripts/mock-server.mjs && pnpm run smoke # 插件链路是否自洽(不依赖真实端点) ``` ## 工作原理 - 插件入口 `apply(ctx, config)`:`ctx.llm.registerConfigurableProviders()` + `ctx.llm.registerAdapter()` + `ctx.llm.registerModelDiscovery()` + `installSettingsSection()`(参考 dsh-llm-ollama 的注册机制)。 - 聊天走 pi-ai 的 OpenAI Chat Completions:`createProvider({ api: openAICompletionsApi(), baseUrl, auth, models })`,每次请求通过 harness 的凭据缝解析 key。 - 连接事实(endpoint / key / 模型目录)**每次操作**重新解析,Settings 里改了立即生效,不用重启。 ## 开发 ```sh pnpm install pnpm run build # tsc(lib/types/*.d.ts)+ tsdown(lib/index.js) pnpm run smoke ``` 构建产物 `lib/` 与声明文件 `lib/types/**/*.d.ts` **提交进 git**(`.gitignore` 只排除 tsc 中间产物),因此 GitHub 安装无需 build-script allowlist——这是与 dsh-hello-tool(依赖 `prepare` 脚本)不同的安装策略。 ## 路线图 - [x] Host 端最小可用版(聊天 + 配置 + 发现) - [ ] Settings → Providers 专属卡片(fetch models 一键导入、模型行内编辑) - [ ] 多 provider 路由(同时挂 vLLM + LM Studio) - [ ] 发布到 npm / GitHub Releases ## License MIT