Models & Providers
dsh-plugin-thinking-api
qjf44/dsh-plugin-thinking-api · ★ 2
One-click OpenAI-compatible API (CodeBuddy/vLLM/proxies) with thinking mode, avoiding developer-role content_filter rejection.
安装
dsh plugin --profile web add github:qjf44/dsh-plugin-thinking-apiGitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
- What it solves
- The bug this fixes
- How it stays maintainable
- Install
- Configure
- One-click setup in the Web GUI
- Configuration reference
- Provider (per providers.<id>)
- Model (per models.<id>)
- Why not llm-pi-ai?
- Versioning: the plugin version IS the DSH version
- Supported DSH
- After upgrading DSH, CodeBuddy / third-party APIs stop working? Run the compat check
- Incident log (2026-10-21, CodeBuddy gateway 11128 "unapproved channel")
- Incident log (2026-09-12, images silently failing)
- Incident log (2026-09-11, DSH 0.1.1-rc.2 → 0.1.5-rc.1)
- Incident log (2026-08-18, DSH rc.6 → rc.7)
- Contributing
- License
简体中文 · English
What it solves
Three problems in one plugin:
- Configure any OpenAI-compatible API in one block — Tencent CodeBuddy, self-hosted vLLM, or any relay — no hand-writing
reasoningEfforts,thinkingFormat, or pi-ai provider internals. - Thinking mode out of the box — set
thinking: trueon a model and it gains reasoning levels automatically. - Fixes the
content_filter/developer-role bug — this is the subtle one.
The bug this fixes
When a model declares reasoning, pi-ai rewrites the system prompt into the OpenAI developer role unless it recognizes the endpoint as a non-standard vendor. Third-party APIs that are not on pi-ai's built-in allowlist (Tencent CodeBuddy is a prime example) therefore receive a developer message, which many endpoints hard-reject with content_filter — while the exact same request using system succeeds.
The official dsh-llm-pi-ai adapter drops the compat.supportsDeveloperRole field when it builds models, so you cannot fix this from settings.yaml alone. This plugin builds the pi-ai models itself and injects compat.supportsDeveloperRole: false directly, forcing the system role.
How it stays maintainable
The plugin reuses the official PiAiAdapter class (exported by @deepseek-ai/dsh-llm-pi-ai): its streaming, chunk translation, credential resolution, idle-timeout watchdog, and image handling all keep evolving with DSH. This plugin only owns the small "model/provider assembly" layer where the correct compat is written in.
Install
Add to your profile's package.json dependencies and bundle list:
// ~/.dsh/profiles/<profile>/package.json
{
"dependencies": {
"dsh-plugin-thinking-api": "github:qjf44/dsh-plugin-thinking-api"
},
"dsh": {
"profile": {
"bundles": [
// ... your existing bundles ...
"dsh-plugin-thinking-api"
]
}
}
}
Then reinstall and restart the Harness. The plugin's cordis.patch.yml registers itself (insert: [{ id: thinking-api }]), so no manual cordis.patch.yml edit is needed.
Configure
Add a thinking-api section to ~/.dsh/settings.yaml:
thinking-api:
providers:
codebuddy: # route id (any unique name)
displayName: CodeBuddy # optional, shown in pickers
baseURL: https://copilot.tencent.com/v2
apiKeyEnv: CODEBUDDY_API_KEY # env-var reference; store via the Web Models page or export it
thinkingFormat: deepseek # optional, default deepseek
models:
deepseek-v4-pro:
name: DeepSeek V4 Pro
thinking: true # ← enables thinking mode
deepseek-v4-flash:
name: DeepSeek V4 Flash
thinking: false
deepseek-v4.1-flash:
name: DeepSeek V4.1 Flash
thinking: true
input: [text, image] # ← declares vision; makes read_image usable
Store the key (never paste raw keys into the config):
# either export it in the launching environment
export CODEBUDDY_API_KEY=ck_xxxxxxxx
# or write it through the Web UI → Models page (credentials service)
Restart, then pick your API's models from the model picker.
📄 Ready-to-copy configs (CodeBuddy / self-hosted vLLM / any OpenAI-compatible relay): see
examples/settings.yaml.
One-click setup in the Web GUI
The plugin also registers a Settings → Thinking API panel with a single setup wizard. Open it to connect an API without touching YAML:
- Source — pick a template (CodeBuddy / DeepSeek / OpenRouter / …) or Custom.
- API key — paste it once. Templates ship sensible default models, so you can save immediately and start chatting.
- Models (optional) — click Fetch models to list the endpoint's models; the wizard pre-flags reasoning models by name (and you can adjust each one with a checkbox). Or add model ids manually.
Tencent CodeBuddy (
copilot.tencent.com) has no OpenAI-compatible/modelsendpoint; Fetch models falls back to the template's default models instead of failing with 404. On other endpoints a 404 means you should enter model ids manually. - Save — done. The provider appears in the model picker and uses the fixed adapter (
supportsDeveloperRole: false).
Existing providers can be edited (the Edit button reloads them into the wizard) or removed.
The client half ships as a browser bundle discovered through
exports["./client"]; after changing plugin source, rebuild the Web artifacts so this URL picks up the new bundle.
Configuration reference
Provider (per providers.<id>)
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
baseURL |
string | ✅ | — | API endpoint base URL |
apiKeyEnv |
string | — | — | Env-var name holding the API key |
displayName |
string | — | route id | Name shown in pickers |
thinkingFormat |
enum | — | deepseek |
deepseek | openai | openrouter | together | baseten | zai | qwen | qwen-chat-template | chat-template | string-thinking | ant-ling |
models |
dict | ✅ | — | Model id → model entry |
Model (per models.<id>)
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
name |
string | — | model id | Display name |
thinking |
boolean | — | false |
Enable reasoning levels |
thinkingEfforts |
dict | — | auto | Custom level → wire-value mapping, e.g. { off: null, high: high, max: xhigh } |
contextWindow |
number | — | 262144 |
Context window size |
maxTokens |
number | — | 32768 |
Max output tokens |
input |
array | — | ["text"] |
Input modalities: ["text"] or ["text","image"]. Only a model declaring image is treated as a vision model by DSH (making read_image usable) |
When thinking: true and no thinkingEfforts is given, the plugin fills a verified DeepSeek-compatible level map (off / high / max, mapping to disabled / reasoning_effort: high / reasoning_effort: xhigh). Provide thinkingEfforts to add levels (low/medium) or override per API.
About
input/ image support: DSH'sread_imagerefuses any route that does not declareimage(model "..." does not declare image input), and this plugin registers every model as text-only by default. To let a model see images, declareinput: [text, image]— e.g. Tencent CodeBuddy'sdeepseek-v4.1-flash, which is the official natively-multimodal V4.1 Flash. An empty array is synonymous with omitting the field and falls back to["text"].This is a declaration only: it does not guarantee the upstream endpoint accepts images. If the endpoint rejects them, you will see an error when an image is actually sent; conversely, if the endpoint supports images but you never declare them, images are blocked locally and never leave your machine.
Why not llm-pi-ai?
The built-in llm-pi-ai adapter already supports custom APIs. It just cannot express supportsDeveloperRole (its compat schema omits the field and its resolver drops it), so thinking-mode models on allowlist-less APIs hit the developer-role rejection. This plugin exists purely to inject that one missing bit, while reusing everything else.
Versioning: the plugin version IS the DSH version
From 0.2.1-rc.2 on, this plugin's version number equals the exact DSH version it is aligned with (previously it incremented independently as 0.1.x). So when you see dsh-plugin-thinking-api@0.2.1-rc.2, you know immediately that it targets DSH 0.2.1-rc.2 — no need to open the changelog to guess. check-docs enforces this: the plugin version must equal the highest DSH version in peerDependencies, or carry one extra patch segment on top of it (e.g. 0.2.1-rc.2.1) for a plugin-only fix, or CI fails.
Versions
0.1.0–0.1.10predate this rule and were numbered independently.0.1.10is the last of them; everything it contained is included in0.2.1-rc.2.
Supported DSH
Built against DSH 0.1.0-rc.6 through 0.2.1-rc.2 (@deepseek-ai/dsh-llm-pi-ai), pi-ai 0.82.1 – 1.x. The plugin defensively reuses PiAiAdapter's constructor shape; if a future DSH changes that internal contract, the plugin fails with a clear error rather than silently misbehaving — check the release notes before upgrading DSH.
DSH 0.2.x. pi-ai 1.3.0 added four thinking dialects (baseten, chat-template, qwen-chat-template, ant-ling) and dsh-llm-pi-ai 0.2.1 accepts all eleven in its SUPPORTED_THINKING_FORMATS. v0.1.10 extends the plugin's thinkingFormat enum and its default effort table to match, and check-compat now diffs the plugin enum against the adapter's source so a future addition fails the check instead of silently rejecting valid config.
DSH 0.1.7 compatibility. 0.1.7 removed SettingsProvider and its installSection() method, replacing them with SettingsForms, which projects the settings page from the plugin's exported Config schema and only exposes fields marked .volatile(). v0.1.9 therefore:
- exports
Config(0.1.7 readsfiber.runtime.Configto build the page), and - marks
providersvolatile, probingvolatile()→extra('volatile', true)in that order — probing.volatile()alone is not enough, see the note below, and - wires a third settings branch (
SettingsForms) beside the existing ≤0.1.1 export-function and 0.1.5/0.1.6 instance-method branches, and - unwraps the cosmokit
Volatilereference that.volatile()produces, soconfig.providersstill enumerates as a plain dict.
Without the volatile mark, 0.1.7's describe() skips this plugin's row entirely and mutate() throws has no volatile fields — the page silently disappears and CodeBuddy can no longer be saved. Without the Config export, the entry is not configurable at all.
Beware a stale schemastery inside your profile. DSH resolves plugin dependencies from the profile's physical
node_modules. If your profile still carries an older copy (e.g.~/.dsh/profiles/<name>/node_modules/@deepseek-ai/schemasteryat 3.18.2 while the 0.1.7 runtime ships 3.18.4), the plugin resolves the old one. That version has novolatilehandling at all, so the mark silently fails and 0.1.7'simportLegacyDocument()drops the entirethinking-apisection ofsettings.yaml(including the CodeBuddy provider directory) after logging only a warning — exit code stays 0 and no error is printed.v0.1.9's
extra('volatile', true)fallback keeps working in exactly that environment (3.18.4's.volatile()is literallyreturn this.extra("volatile", true), so the two are equivalent), so you do not need to delete that copy, and it survivespnpm installre-creating it. Deleting the directory is still fine if you prefer. Do not copy 3.18.4 into an old profile in reverse: it needs cosmokit ≥1.8.5 (old profiles have 1.8.3) and will make DSH 0.1.6 fail to boot outright.
DSH 0.1.5 compatibility. 0.1.5 moved several internal contracts; versions ≤ 0.1.2 break on it. v0.1.3–v0.1.5 fix them in turn (see the changelog for the full detail):
| Symptom on DSH 0.1.5 | Root cause | Fixed in |
|---|---|---|
Failed to load plugins at boot |
dsh.client.inject still declared the removed @deepseek-ai/dsh-client-runtime |
v0.1.3 |
Settings panel: Cannot read properties of undefined (reading 'settings') |
connection no longer exposes .api; reads must go through ctx.remote.* |
v0.1.4 |
Model picker: CodeBuddy 加载失败: Cannot read properties of undefined (reading 'get') |
0.1.5 pi-ai modelOf() reads profile.modelErrors, which the plugin's hand-built profile omitted |
v0.1.5 |
After upgrading DSH, CodeBuddy / third-party APIs stop working? Run the compat check
The plugin depends on several internal contracts of pi-ai and DSH (provider auth shape, PiAiAdapter constructor, llm service registration methods). These are not covered by semver. So after every DSH (or pi-ai) upgrade, if models suddenly fail with Provider is not configured, content_filter, or MISSING_CREDENTIAL, run the compat check first:
# from the plugin repo (DSH workspace is auto-detected upward; pass --workspace when needed)
node scripts/check-compat.mjs --workspace ~/.workbuddy/binaries/node/workspace
It verifies, item by item: the plugin imports under the real dependencies, the PiAiAdapter constructor shape, the pi-ai provider auth shape, the llm service registration methods, and the settings/credentials helpers. All ✓ means you can keep using it; any ✗ means the plugin needs an update (each failure names the changed contract and where to fix it).
Incident log (2026-10-21, CodeBuddy gateway 11128 "unapproved channel")
Symptom: every CodeBuddy / CodeBuddy2 request failed with
400 {"code":11128,"msg":"Illegal API invocation from an unapproved channel"},
while curl with the same key and the same body shape returned 200.
The gateway fingerprints the client identity sentence in the system prompt, not
the User-Agent and not the message roles. The exact trigger is the whole sentence
You are an AI agent powered by DeepSeek Harness. (case-insensitive). Each fragment
alone passes: You are an AI agent., You are powered by DeepSeek Harness.,
You are an AI agent powered by Cursor. — and so does any wording that breaks the
"powered by" adjacency. Insert the full sentence and you get 11128 back.
Control experiments run with a real key (all 200 unless noted): tools, 1.3 MB body,
45 tools, image data-URI, assistant+tool transcript, every optional field. Feeding
the real system prompt failed even truncated to its first 500 characters, while the
same text in a user role passed.
Two lessons worth keeping:
- "My curl works, the plugin doesn't" does not mean the plugin is broken. Here the only difference that mattered was content, not transport. When bisecting, bisect the body text too, not just headers and roles.
- A
developer-role rejection can be a red herring. It reproduces the identical error string, so it looks like the root cause. It was a real failure mode (now defended), but not the one the user was hitting.
Fix: three defence layers, all in the buildProvider stream/streamSimple wrapper —
compat supportsDeveloperRole: false, an onPayload hook rewriting developer →
system, and a fetch-level rewrite of the identity sentence (the fetch layer is
the last stop, reached by every code path). Observability: each request logs its
real wire shape to /tmp/dsh-thinking-api-wire.log, and a 11128 dumps the complete
request body to /tmp/dsh-thinking-api-wire-fail-<ts>.json.
Incident log (2026-09-12, images silently failing)
A user reported that CodeBuddy's deepseek-v4.1-flash could not read images. The verdict was the plugin, not the model:
- Symptom.
read_imageon any picture failed withmodel "<id>" does not declare image input; switch to an image-capable model to read images. Note this refusal happened before any network request was made. - Root cause.
buildModel()hard-codedinput: ['text'], anddsh-llm-pi-aimaps that verbatim ontoinputModalities;dsh-tool-fs'sread_imagethen refuses any route that does not declareimage. So the image was dropped locally — the upstream being natively multimodal made no difference. The model-entry schema had noinputfield at all, so users could not work around it fromsettings.yaml. - Fix. Added an optional
inputfield (defaulting to['text'], so existing behavior is unchanged) and madebuildModelread it; an empty array is synonymous with omitting it, matching upstreamdeclaredInputsemantics.
Lesson: "the model doesn't support it" and "the route never declared it" are different failures. When debugging vision, check the local capability declaration (inputModalities) before suspecting the endpoint — the former blocks images locally, the latter errors server-side, and the two are fixed in completely different places.
One more deployment trap worth remembering: a file: dependency in the profile is a copy, not a symlink. Editing the plugin repo without re-running pnpm install leaves the profile's stale copy untouched and the change silently inert.
Incident log (2026-09-11, DSH 0.1.1-rc.2 → 0.1.5-rc.1)
Three contracts moved at once, each surfacing only at a different layer — which is why a "server boots fine" check misses them:
- Client bundle preload (
Failed to load plugins). 0.1.5 dropped@deepseek-ai/dsh-client-runtime, but the plugin still declared it indsh.client.inject, so the loader missed the module and the whole plugin graph failed.createSnapshotStorealso moved to the built-in seed module@deepseek-ai/dsh-client-store. Fix: drop the runtime frominject; import the store from the seed module. - Renderer data layer (
reading 'settings').connectionno longer carries.api; the settings/credentials/llm reads had to move to the cordis namespace servicesctx.remote.settings/ctx.remote.credentials/ctx.remote.llm. Note the client bundle is served per request with a content-hash rev, so this half takes effect on a page reload — no host restart needed. - Host profile shape (
reading 'get'). 0.1.5'spi-aimodelOf()unconditionally evaluatesprofile.modelErrors.get(model). The plugin builds its profiles by hand (it cannot reuse the officialresolveProfiles, which has nouserAgent/compat.supportsDeveloperRolehook), and that hand-built object predated the field — somodelCatalogenumeration threw for the whole provider group.listModelsalone usesgetModels()and stays safe, so the group listed fine and only failed once the selector resolved per-model details. Fix: emitmodelErrors: new Map()plus the image-budget defaults the official profile also carries. This half is host code and does need a harness restart.
Lesson: after a DSH upgrade, verify at all three layers (boot, settings panel, model picker) — not just that the server starts.
Incident log (2026-08-18, DSH rc.6 → rc.7)
After the upgrade, codebuddy requests failed 100% with PI_AI_ERROR: Provider is not configured: codebuddy. Root cause: pi-ai 0.82.1 changed the provider auth contract:
- Before 0.82.1:
auth: { name, resolve }(top-levelresolve). - From 0.82.1:
resolveProviderAuthonly recognizesauth.apiKey.resolve; a top-levelresolveis treated as "no auth method" →getAuthreturns nothing → the error above.
Fix: buildProvider now assembles auth as { apiKey: { name, resolve } }, matching the official dsh-llm-pi-ai routeAuth/harnessApiKeyAuth shape. The plugin also self-checks this contract at startup, so a version mismatch now fails loudly at boot instead of on your first message.
Second pitfall: editing a provider in the GUI wizard with the key field left blank wiped the existing apiKeyEnv, losing the credential reference after an upgrade/reconfigure (reported as MISSING_CREDENTIAL or auth failures). Fixed: in edit mode, a blank key means "keep unchanged" and the previous apiKeyEnv is preserved.
Contributing
Bugs and reports welcome — please open an issue via the bug report template (it asks for the DSH version, plugin version, and the compat-check output, which covers 90% of failures).
Before opening a PR, run the local checks:
npm run check # syntax (lib/index.mjs, lib/client.js, scripts/check-compat.mjs)
node scripts/check-compat.mjs --workspace ~/.workbuddy/binaries/node/workspace # contract check against your real DSH
CI runs npm run check plus an npm-package sanity job (tarball file list and version-bump) on every push/PR — see .github/workflows/check.yml.