[中文](./README.md) · [English](./README.en.md) · [日本語](./README.ja.md) # dsh-deepseek-relay — Reasoning-effort adapter for DeepSeek relay stations > Make DeepSeek models imported into deepseek-harness (`dsh`) through a **relay station** (a third-party OpenAI-compatible gateway such as OneAPI / new-api / one-hub, or any "API forwarding" service) expose the same **reasoning level (thinking intensity)** control in the Web UI as the official API: Off / Low / High / Max, and send the thinking parameters in the dialect the relay understands. ## ⚠️ Upgrade note (v0.1.1, important) If you are upgrading from an older version, there is exactly **one place you need to check manually**: > **The only thing you must edit by hand** > > If your old config has a model that actually supports vision (e.g. `deepseek-v4-flash-vision-exp`), but you previously wrote only its `id` with no capability fields — after upgrading it **defaults to "does not support vision"**. To let it receive images, manually add one line: > > ```yaml > - id: deepseek-v4-flash-vision-exp > supportsVision: true # ← add this line yourself if you want vision after upgrading > ``` > > Other (text-only) models need no change. `supportsVision` defaults to `false` when omitted — this is by design (so the host surfaces a friendly "model does not support images" error instead of sending the request and failing). ## Features - ✅ Exposes Off / Low / High / Max reasoning levels in the Web UI (identical to official `llm-deepseek`) - ✅ Serializes thinking parameters into the relay's dialect (`openai` / `deepseek`) automatically - ✅ Per-model wire-value overrides for gateways that require custom values like `ultra` - ✅ Per-model vision capability declaration (`supportsVision`) with friendly image rejection - ✅ One plugin instance can mount multiple relay routes - ✅ Works as a pure plugin — **no changes to the harness core required** ## The problem The official API goes through the built-in `llm-deepseek` adapter, whose `resolveModel` returns `reasoning.efforts = [Off, Low, High, Max]`, so the UI shows a reasoning-level option. Relay stations usually go through `llm-pi-ai`'s "custom provider" (`openai-completions`). But: 1. The Web UI form has no `reasoningEfforts` field; hand-written models carry no reasoning metadata, so the reasoning-level option **simply does not appear** in the UI; 2. Even if declared, the thinking-parameter format sent to the relay may be wrong — DeepSeek's official format (`thinking: {type: "enabled"}` + `reasoning_effort`) and the OpenAI format (only `reasoning_effort`) are not universally accepted by relay stations. ## The solution This plugin registers an **independent OpenAI-compatible adapter route** (one route per relay station), and: - **Always exposes Off / Low / High / Max to the UI** (identical to the official `llm-deepseek`, see `REASONING_EFFORTS` in `src/adapter.ts`); - Serializes the thinking parameters into the dialect the relay recognizes, based on `thinkingFormat` (see `src/serialize.ts`): | Level | `openai` (default) | `deepseek` | | --- | --- | --- | | off | no thinking field sent (gateway default) | `thinking: {type:"disabled"}` | | low | `reasoning_effort: "low"` | `thinking:{type:"enabled"}` + `reasoning_effort:"low"` | | high | `reasoning_effort: "high"` | `thinking:{type:"enabled"}` + `reasoning_effort:"high"` | | max | `reasoning_effort: "max"` | `thinking:{type:"enabled"}` + `reasoning_effort:"max"` | - `thinkingFormat: auto` guesses by baseURL host: contains `deepseek.com/ai/cn` → `deepseek`, otherwise default `openai` (most relays). - Supports per-model wire-value overrides (`reasoningEfforts`) for gateways that require custom values like `ultra`. ## Usage Both methods comply with the official plugin spec (see "Official spec compliance" below): ### Method A: Run from source, load locally with `--patch` (dev / personal) At the root of the deepseek-harness repo (already `pnpm install`ed): ```sh pnpm dsh web --patch ./dsh-deepseek-relay/cordis.yml ``` Before starting, edit `cordis.yml` and replace `providers.relay` with your relay info: ```yaml - insert: - id: dsh-deepseek-relay name: '/absolute/path/deepseek-harness/dsh-deepseek-relay/src/index.ts' config: providers: my-relay: baseURL: https://your-relay.example.com/v1 apiKeyEnv: RELAY_API_KEY thinkingFormat: auto models: - id: deepseek-v4-flash - id: deepseek-v4-flash-vision-exp supportsVision: true # enable for vision-capable models ``` - **API key**: set the `RELAY_API_KEY` env var, or after launch open Web UI **Settings → Models**, select this provider and fill in the key (stored in `$DSH_HOME/.credentials.yaml`). - **Models**: the `models` list is what appears in the model selector; `contextWindow` / `maxTokens` become the context/output limits. - After saving, in a conversation that has this model selected, a **Off / Low / High / Max** reasoning-level dropdown appears next to the input box (same as the official API). ### Method B: Install as a bundle (distributable) ```sh # local dir / git / tarball all work; initializes profile on first run dsh plugin --profile demo add ./dsh-deepseek-relay # or dsh plugin --profile demo add github:you/dsh-deepseek-relay ``` The package's `cordis.patch.yml` declares the plugin line (`name: dsh-deepseek-relay` resolved by package name, loads `lib/index.js`); the `prepare` script (esbuild) builds `lib/` automatically at install time. After install the plugin is **dormant (empty config is safe to load, registers no routes/directory; both the adapter side and the configurable-provider directory side have empty-array guards — see `ensureDirectory`/`ensureRegistrationFacts` in `src/index.ts`)**. Override the same-id line in the profile's `cordis.patch.yml` or `$DSH_HOME/cordis.patch.yml` to fill in relay config (see the top comment of `cordis.patch.yml`), or hot-update later in the Web UI settings. ## Configuration | Field | Description | Default | | --- | --- | --- | | `baseURL` | Relay OpenAI-compatible address; `/chat/completions` is appended automatically | required | | `apiKeyEnv` | API key environment variable name | `RELAY_API_KEY` | | `displayName` | Model selector display name | route key | | `thinkingFormat` | `auto` / `openai` / `deepseek` | `auto` | | `reasoningEffort` | Default reasoning level `off`/`low`/`high`/`max` | `high` | | `maxTokensField` | Output limit field `max_tokens`/`max_completion_tokens` | `max_tokens` | | `maxTokens` | Route-level default output limit | 256000 | | `models[].id` | Model id sent to the gateway | required | | `models[].name` | Selector display name | id | | `models[].contextWindow` | Context capacity | 262144 | | `models[].maxTokens` | This model's output limit | route-level value | | `models[].reasoningEfforts` | Per-level wire-value override (`null` = supported but not sent) | dialect default | | `models[].supportsVision` | Whether the model accepts image input (rendered in the Web UI as the model-row "支持识图" checkbox). `true` → `inputModalities:['text','image']`; `false` or omitted → `['text']` (**explicitly not undefined**, so the host surfaces the friendly "model does not support images" error when an image is attached) | `false` | > 💡 **For upgraders**: if an old model actually supports vision but you only wrote its `id`, it will be treated as `supportsVision: false` after upgrade — add `supportsVision: true` as shown in "Upgrade note" above to enable vision. ## Official spec compliance Checked against `docs/user/develop/basic/{index,config,publish}.zh.md` and `docs/user/develop/practice/llm-adapter.zh.md`: | Official requirement | This plugin | | --- | --- | | Plugin module exports `name` + `apply(ctx, config)` | ✅ `src/index.ts` | | Declare `inject` (this plugin depends on the `llm` service) | ✅ `inject = ['llm']` | | Export `Config` type + same-named Schemastery schema, defaults in schema | ✅ `src/index.ts` | | `--patch` overlay loads local plugin (source `.ts` path) | ✅ `cordis.yml` (Method A) | | Bundle `dsh.bundle` manifest + `cordis.patch.yml` (referenced by package name) | ✅ `package.json` + `cordis.patch.yml` (Method B) | | Git-installed TS package must ship a `prepare` build (produces `lib/`) | ✅ `scripts/build.mjs` + `tsconfig.build.json` | | `LlmAdapter` contract: `stream()`, `resolveModel` returns `reasoning` metadata, `attributionHeaders()`, stable `LlmError` code, passes through `options.signal` | ✅ `src/adapter.ts` | Note: loading `.ts` from a source path via `--patch` is an official-tutorial-supported **dev method**; formal distribution (npm/git/tarball) uses Method B, where the `prepare` build produces `lib/index.js` and is equivalent to running from source. Both share the same `src/`. ## Alternative: no plugin — configure official llm-pi-ai by hand The official `llm-pi-ai` already supports the same capability; you just have to hand-write `$DSH_HOME/settings.yaml`. If you'd rather not install the plugin, write it like this (equivalent to `thinkingFormat: deepseek`): ```yaml llm-pi-ai: providers: my-relay: apiKeyEnv: RELAY_API_KEY api: openai-completions baseURL: https://your-relay.example.com/v1 compat: thinkingFormat: deepseek models: - id: deepseek-v4-flash reasoningEfforts: off: low: low high: high max: max ``` > Note: in `reasoningEfforts`, an empty `off:` (nothing after the colon) means "supports this level, sends nothing"; the other levels must give a wire value, otherwise the config is rejected. Most gateways (OneAPI, etc.) should use `thinkingFormat: openai` rather than `deepseek`, unless the gateway forwards the official API as-is. ## Code structure ``` dsh-deepseek-relay/ ├── cordis.yml # --patch local-load config example (Method A) ├── cordis.patch.yml # bundle config layer (Method B, dsh.bundle.patch points to it) ├── package.json # dsh.bundle manifest + scripts (prepare build) ├── scripts/build.mjs # esbuild single-file lib/index.js (external @deepseek-ai/*) ├── tsconfig.json # type check (tsc --noEmit) └── src/ ├── index.ts # plugin entry: Config schema, route registration, settings hot-update ├── adapter.ts # RelayAdapter: four reasoning levels + fetch/SSE stream ├── serialize.ts # message + thinking-parameter serialization (openai/deepseek dialects) ├── translate.ts # SSE chunk → harness StreamChunk ├── sse.ts # SSE parsing (eventsource-parser) └── types.ts # wire types ``` ## Verification - `tsc --noEmit` full type check: 0 errors (against official npm packages `@deepseek-ai/dsh-llm` etc. 0.1.1-rc.2). - Serialization logic tests (`resolveThinking` / `resolveThinkingFormat` / `serializeRequest`, openai/deepseek dialect × off/low/high/max × model-level override × session-title × maxTokensField): all pass. - SSE conversion tests (`reasoning_content` stream, tool_calls incremental concatenation, usage deduplication, `[DONE]` termination, empty response): all pass. - Test deps installed flat via npm (`C:/Users/29261/Downloads/relay-verify`, test scripts reusable). ## Notes - If the relay has a `/v1` suffix in addition to `/chat/completions`, write the full path in `baseURL`, e.g. `https://relay.example.com` (if the gateway happens to require no `/v1`). - The plugin accepts text input only by default (consistent with official `llm-deepseek`); image input is rejected with `UNSUPPORTED_CONTENT`. If a model declares `supportsVision: true` but image transport (attachments store integration) is not yet implemented, it returns `UNSUPPORTED_CONTENT` ("image transport not implemented yet") — to be added later. - One plugin instance can configure multiple relay routes (just add to the `providers` dict). ## Disclaimer This is an unofficial third-party adapter layer with no affiliation to DeepSeek. When using a relay station / third-party gateway, comply with its terms of service and applicable laws. **The plugin is provided "as is", without warranty that it fits every gateway, and assumes no responsibility for any data loss, config corruption, or account risk that may arise from installation, use, or upgrade.** Back up `$DSH_HOME` (including `settings.yaml` and `.credentials.yaml`) before upgrading. Clicking this repository's links means you have read and agree to the above.