DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Llm Session Header — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins

dsh-llm-session-header

Llm Session Header

DeepSeek Harness 插件:为路由到已配置提供商的模型请求附加稳定的每会话请求头。请求头名称可配置,因此一个插件即可服务于 SMG(X-SMG-Routing-Key)、OpenCode(x-opencode-session)以及任何其他请求头-bas

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add dsh-llm-session-header@0.4.0
README兼容性版本

兼容性与来源证明

Llm Session Header 以 dsh-llm-session-header 发布,当前版本为 0.4.0。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
npm
Registry 更新时间
2026/10/16

版本

0.4.0stable
2026/10/2
0.3.1stable
2026/9/30
0.3.0stable
2026/9/30

相关插件

正在加载相关插件…

最新版
0.4.0
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
71.8 kB
文件数
9
Surface
web
许可证
MIT
发布源
npm
周下载
23
查看源码 ↗项目主页 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 models-usage 分类下经过校验的插件。

Usage@linxin666/dsh-usage用于 dsh Web GUI 的使用统计插件:检测各提供商的余额和编程套餐配额,并实时记录令牌使用情况;侧边栏 Settings 行上方显示紧凑的价格概览卡片Client Ui Model Capabilities@linxin666/dsh-client-ui-model-capabilities为自定义 pi-ai 提供商声明每个模型的能力(图像输入和推理力度),可直接在 Models 设置卡片中编辑;写入官方 llm-pi-ai 设置命名空间,不更改 DSH 源代码。Codex Subscriptiondsh-codex-subscription通过 OAuth 在 DeepSeek Harness 中使用 ChatGPT 和 Codex 订阅,支持配额管理、安全重置、网页搜索、图像和快速模式Commandcode Provider@mars-sea/dsh-commandcode-provider适用于 Command Code 的非官方 DeepSeek Harness LLM 提供商插件,移植自 pi-commandcode-provider(MIT)。注册“commandcode”提供商路由,并在 Models 页面添加卡片和实时模型目录。

README

dsh-llm-session-header

A DeepSeek Harness host plugin that attaches a stable per-conversation session header to model requests routed to configured providers.

The header name is configurable, so one plugin serves any backend that implements conversation-affinity routing via a request header — SMG's X-SMG-Routing-Key, OpenCode's x-opencode-session, and others.

Verified againstdsh 0.1.7-rc.2 and 0.2.0-rc.2 (session format 4)
RequiresNode ^22.19 || >=24; schemastery >=3.18.4 for the settings card
On older dshthe plugin loads and injects as usual; the settings card is simply absent

Nothing needed to change for 0.2.0. The llm/stream contract, the settings service, the settings API controller and the client settings layer are all byte-identical between 0.1.7-rc.2 and 0.2.0-rc.2; only the client primitives and plugin-manager bundles moved, and the card renders correctly on the new ones. Verified in an isolated 0.2.0 instance: the card renders every control, a save persists to the profile patch, and a route added through the card after activation injects its header on the next model call (checked on the wire at a header-recording mock).

npm test runs tests/compat-0.2.mjs, which pins the schema and the live-save path against a 0.2.0 install when one is pointed at: DSH_020=<prefix> npm run test:0.2.

Upgrading on a pnpm-managed profile. dsh profiles installed with pnpm enforce a supply-chain minimum-release-age policy, so a version published minutes ago is refused until it is listed in minimumReleaseAgeExclude in ~/.dsh/profiles/<profile>/pnpm-workspace.yaml. Keep the old entry listed while you upgrade — the policy checks the versions pinned in pnpm-lock.yaml, so replacing it too early fails on the version you are leaving. Then restart the service that reads the profile.

Why

Some LLM relays pin every request sharing the same session header value to the same upstream backend, which keeps the prompt/KV cache warm across the turns of one conversation. Without the header those relays either reject the request (OpenCode returns 400 MissingSessionID) or route each turn to an arbitrary backend, discarding the cache every turn.

The value only has to be opaque and stable per conversation, so the plugin reuses the DSH session id that already travels with each model call — the same identity the official DeepSeek adapter sends as x-deepseek-harness-session-id.

Why the header is injected rather than derived

A relay's alternative to an explicit key is deriving stickiness from a prefix hash of the prompt. That approach does not work for DSH, and it was verified experimentally rather than assumed: DSH sends a large shared system prompt, so distinct sessions hash to the same value and pin to the same backend. Extending the hashed prefix cannot fix it, because the shared preamble is the hash input.

The plugin is therefore not an optimisation on top of a working router — it is what makes per-conversation pinning work at all.

Install

dsh plugin --profile <profile> add dsh-llm-session-header

Or from a local checkout:

dsh plugin --profile <profile> add /abs/path/to/dsh-llm-session-header

The package declares dsh.bundle, so dsh plugin add reconciles it into dsh.profile.bundles automatically and it becomes a profile layer. Restart dsh once for the change to take effect.

On boot you should see:

[llm-session-header] active for providers [opencode=x-opencode-session] with mode session-id

Configuration

The row above is inserted by this package's own bundle layer with the defaults shown. A profile customises it with an id-targeted config override, never a second insert:

- id: llm-session-header
  config:
    headerName: x-opencode-session   # default
    providers: [opencode, opencode-go]
    mode: session-id
    debug: false
FieldDefaultMeaning
headerNamex-opencode-sessionHeader to attach when providers is a list
providers[opencode, opencode-go]Which route keys get a header, and which header
modesession-idsession-id reuses the DSH session id; uuid derives a process-stable uuid
debugfalseLog every streamed call that receives a header
debugFile—Append one JSON line per injected call to this path

providers accepts two shapes

A list — every listed route gets headerName:

config:
  headerName: X-SMG-Routing-Key
  providers: [b70-smg]

A map — each route names its own header, overriding headerName:

config:
  providers:
    opencode: x-opencode-session
    opencode-go: x-opencode-session
    b70-smg: X-SMG-Routing-Key

A list and a map cannot be mixed. The map form is how one plugin instance serves multiple backends with different header names. It is also what makes swapping the router behind a route a one-line config change:

-      b70-olla: X-Olla-Session-ID
+      b70-smg: X-SMG-Routing-Key

Editing from the Web UI

The plugin ships a settings card, so the whole mapping is editable from the Plugins page without touching YAML:

Plugins → Installed → LLM session header → the llm-session-header component → Configure.

The card edits:

  • the default header name, the value mode and the debug flag;
  • the route table itself — Add route appends a row, each row names a route key and (optionally) its own header, and Remove drops one.

Each route is picked from a dropdown of the routes the deployment actually composes, so a typo cannot silently produce a route that never matches. The list is read from the composed configuration (the llm-pi-ai providers, the agent-default-model route, and the llm-* namespaces), not hardcoded. A route already in your config but not in that list is kept as its own option, and + Custom route… switches a row to a free-text field for anything the list does not know about.

The plugin's display name is localised. locale/en.json and locale/zh.json carry the title and description the Plugins page shows, so the bundle appears as "LLM session header" / "LLM 会话路由请求头" rather than as the package name. Anything keying on the package name in the UI — a script, a bookmarklet, a test selector — must use the localised title instead.

Why it lives on the row, not in the Official list. The page's slot contract marks plugins.item as "OCCUPIED by the official settings pages, one companion package per host-plane namespace; a bundle's configuration belongs in plugins.bundle.config or plugins.row.config instead". A third-party bundle therefore registers into plugins.row.config, keyed dsh-llm-session-header#llm-session-header, which is what puts a Configure button on the row. Our settings namespace is the row id, so the row is the right owner. (Verified on both dsh 0.1.7-rc.2 and 0.2.0-rc.2, whose slot contracts are byte-identical.)

Leave a row's header blank to inherit the default header name. Saving writes the whole providers field in one fenced revision-checked mutation, so a concurrent edit elsewhere in the profile is rejected rather than silently overwritten.

Saves take effect on the next model call — no restart. Every field is declared .volatile(), so the Host rewrites the live config reference in place instead of remounting the plugin; the route table is re-resolved on each llm/stream call.

Serving OpenCode and SMG together

The map form is what the production profile actually uses — one row covering every route, each with the header name its own backend expects:

- id: llm-session-header
  config:
    headerName: x-opencode-session
    providers:
      opencode: x-opencode-session
      opencode-go: x-opencode-session
      opencode-go-v41: x-opencode-session
      b70-smg: X-SMG-Routing-Key
    mode: session-id
    debug: false

Note: the config of a patch row replaces wholesale — it does not merge. Restate every key you want to keep, headerName included.

Do not insert this row from a profile patch. The package ships its own dsh.bundle layer that already inserts the llm-session-header row, so a second insert at the profile layer produces a duplicate row id and a boot failure. Use the id-targeted config form above, exactly as shown.

Verifying the deployed composition

dsh --profile <profile> --dump-config | grep -A11 "id: llm-session-header"

Expect exactly one row, carrying name: dsh-llm-session-header.

How it works

  1. llm/stream waterfall — calls whose options.provider is a configured route and which carry a sessionId are wrapped.
  2. AsyncLocalStorage — the header name and value are held in an ALS store. The plugin wraps the downstream iterator so every pull executes inside als.run(...), which is what makes the value visible to the code that eventually issues the HTTP request.
  3. globalThis.fetch patch — patched once, fiber-scoped via ctx.effect. While a store is active it merges <headerName>: <value> into the outgoing request headers. An existing header of that name always wins.

On plugin stop/update/unload the listener is removed and the original fetch restored.

Requests that are not routed to a configured provider, requests with no sessionId, and model-discovery requests pass through untouched.

Verifying a live deployment

The header is injected into an outgoing request, so a passing unit test is not proof that a deployed router is receiving it. Check the routing decider, not a counter.

SMG echoes the worker it chose on every response:

x-smg-routed-worker-id: http://<worker-host>:<port>

Send several requests carrying the same routing key and confirm this header always names the same worker. That is stable per-key assignment — the exact property this plugin exists to produce — and it is directly observed rather than inferred.

  • Same worker every time → stickiness works → the header was received.
  • Worker varies across requests → the key is not driving assignment.

Do not verify with GET <smg>/workers' load field. It is a single field that does not distinguish "assigned routing keys" from "in-flight requests", and on current builds it renders the latter — so it reads 0 whenever the gateway is idle, including while sticky routing is working correctly. Relying on it produces both false negatives and (as happened here once) false positives. See docs/smg-load-field-correction.md for the measurement and the reasoning.

Notes and limitations

  • Provider keys are matched case-sensitively against options.provider. A custom route key must be listed explicitly — it is not inferred.
  • The model-listing flow (GET <baseURL>/models) does not receive the header. It is a separate code path and is not session-scoped.
  • The plugin relies on DSH outbound LLM requests going through Node's global fetch. If a future dsh version changes its network stack, injection stops (symptom: the upstream rejection returns).
  • Only one llm/stream listener is registered per plugin instance. Do not mount two rows of this plugin with overlapping provider sets — the outer wrapper would win and inner configuration would be ignored. Use one row with the map form instead.

Development

# Run full test suite
npm test

# Build client bundle from src/client/
npm run build:client
LayerFileWhat it proves
Unittests/test.mjsConfig resolution, value modes, fetch injection, stream wrapping, BoundedMap, and schema/locale metadata
Config updatestests/config-update.test.mjsA live save reaches a running instance through volatile references
Integrationtests/integration.mjsReal fetch patch + real HTTP to a local mock; per-route headers
Slot contracttests/slot-contract.mjsVerifies plugins.row.config registration and contract compliance
Coexistencetests/coexistence.mjsByte-parity with the original, and both mounted together

Client Bundle Architecture (src/client/)

The client settings card is modularized under src/client/:

  • constants.js: Slot keys (PACKAGE, ENTRY_ID), modes, default headers, and styles.
  • locales.js: Localization dictionaries (en, zh).
  • store.js: Minimal zustand-compatible state store (createLocalStore).
  • controller.js: Draft management and revision-checked save mutation (CardController).
  • card.js: Presentational React components (LlmSessionHeaderCard).
  • lifecycle.js: Plugin lifecycle hook and plugins.row.config slot registration (apply).

To build the client bundle into lib/client.js, run npm run build:client.

lib/client.js is a committed build artifact. The browser only ever loads that file, so editing src/client/ without rebuilding ships a stale card — and nothing fails: the bundle still parses and the tests still pass, because they exercise whatever is committed. tests/client-bundle.test.mjs loads the built bundle through a stub module loader (so a broken concatenation is caught rather than assumed), and CI and the publish workflow both rebuild and git diff --exit-code lib/client.js, so drift fails the build instead of reaching npm.

Settings Page Row Metadata

Row title and description shown on DSH's Plugins list page are resolved by @deepseek-ai/dsh-app-boot from locale/en.json and locale/zh.json:

  • English: LLM session header
  • Chinese: LLM 会话路由请求头

The integration test boots the plugin, patches fetch, drives a real HTTP request through a local mock, and asserts per-route header routing. It covers the shipping b70-smg route by name, plus x-opencode-session parity.

To render the settings card in a throwaway instance (never production), see the workspace skill .dsh/skills/dsh-plugin-settings-card/; it provisions an isolated DSH_HOME, a prefix with its own dsh, and a mock provider.

Never point a test at production DSH. Use an isolated DSH_HOME under a durable path (not /tmp, which can be swept between commands), and set it in the same shell invocation as the command:

ISO=~/dsh-build/iso-home
DSH_HOME="$ISO" dsh plugin --profile boot-test add /abs/path/to/this/plugin

If the variable is lost, DSH_HOME silently falls back to ~/.dsh and the command writes to the production profile. Verify isolation afterwards.

Credits

Derived from dsh-opencode-session by nobu121 (MIT), generalised from a hardcoded header to a configurable one.

License

MIT