dsh-opencode-go-router 0.0.0-stage → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +22 -0
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +135 -2
- package/README.zh-CN.md +136 -0
- package/cordis.patch.yml +5 -0
- package/lib/core.js +345 -0
- package/lib/impl.js +428 -0
- package/lib/index.js +53 -0
- package/lib/monitor.js +528 -0
- package/lib/store.js +202 -0
- package/package.json +62 -4
package/CHANGELOG.en.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. Versioning follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [0.1.6] - 2026-11-08
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Fixed path portability in the test suite on Linux CI and wait for pending state writes before removing temporary directories.
|
|
11
|
+
|
|
12
|
+
## [0.1.5] - 2026-11-08
|
|
13
|
+
|
|
14
|
+
First public release.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Tracks billion-context fold records and rotates `x-opencode-session` after the next rebuild when the last rebuild's cache hit is at or below the configured threshold (25% by default) and the last five requests are slow by TTFT or throughput.
|
|
19
|
+
- Detects system-prompt rewrites before request headers are assembled, bypasses the fold cache condition for the rewrite request, still requires slow samples, and merges the route switch with the rewrite's unavoidable cache miss.
|
|
20
|
+
- Applies only to OpenCode Go conversations using a `deepseek*` model; non-target requests never trigger a rotation or create a monitor.
|
|
21
|
+
- Stays idle without billion-context and exposes live state at `GET /opencode-router/status`.
|
|
22
|
+
- Persists routes, avoids rotating twice for the same fold, and guards against a fold rotation right after a system-rewrite rotation.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 更新日志
|
|
2
|
+
|
|
3
|
+
本项目的所有重要变更都记录在这里,版本号遵循
|
|
4
|
+
[语义化版本](https://semver.org/lang/zh-CN/)。
|
|
5
|
+
|
|
6
|
+
## [0.1.6] - 2026-11-08
|
|
7
|
+
|
|
8
|
+
### 修复
|
|
9
|
+
|
|
10
|
+
- 修复测试在 Linux CI 上的路径可移植性问题,并在清理临时目录前等待状态文件写入完成。
|
|
11
|
+
|
|
12
|
+
## [0.1.5] - 2026-11-08
|
|
13
|
+
|
|
14
|
+
首个公开版本。
|
|
15
|
+
|
|
16
|
+
### 新增
|
|
17
|
+
|
|
18
|
+
- 监控 billion-context 的折叠记录:当最近一次重建后的缓存命中不高于配置阈值(默认 25%),且最近 5 次请求的 TTFT 或吞吐达到慢速条件时,在下一个折叠重建后轮换 `x-opencode-session`。
|
|
19
|
+
- 系统提示词改写检测:在 `llm/stream` 的请求头生成前比较初始 system prompt 指纹;改写请求跳过折叠缓存条件,但仍要求最近请求变慢,并把换路由的冷请求与改写本身的冷请求合并。
|
|
20
|
+
- 仅对 OpenCode Go 的 `deepseek*` 模型会话生效;非目标请求不会触发轮换,也不会创建监控。
|
|
21
|
+
- 无 billion-context 时保持空闲;状态可通过 `GET /opencode-router/status` 查看。
|
|
22
|
+
- 自动轮换、手动/持久化路由、同一折叠不重复轮换、系统改写后的折叠防重。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TLEWA and dsh-opencode-go-router contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,136 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-opencode-go-router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/TLEWA/dsh-opencode-go-router/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-opencode-go-router)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
[English](./README.md) · [简体中文](./README.zh-CN.md)
|
|
8
|
+
|
|
9
|
+
A DeepSeek Harness (DSH) host plugin that gives an OpenCode Go conversation using a DeepSeek-series model a fresh routing id when billion-context compression loses the reusable prompt cache **and** recent requests are slow, or when a system-prompt rewrite is about to make the next cache miss unavoidable.
|
|
10
|
+
|
|
11
|
+
This is the npm package for the DSH plugin whose internal id is `opencode-router`. It only rotates the OpenCode Go routing header; it is not a model adapter.
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
OpenCode Go pins a conversation to one upstream backend with the `x-opencode-session` request header. After billion-context rebuilds the context, the next request normally warms a new prefix on that same backend. If the first request after a rebuild reports a low cache hit (25% or less by default), and the last few requests are also slow, the backend is probably a bad place for this conversation. At the **next** compression rebuild this plugin rotates `x-opencode-session`, so the next request is sent to a different backend.
|
|
16
|
+
|
|
17
|
+
Only OpenCode Go DeepSeek-series conversations are eligible. Other models are never routed by this plugin, even when a route exists.
|
|
18
|
+
|
|
19
|
+
A system-prompt rewrite invalidates the reusable prefix by itself. The request that carries the rewrite is cold on any backend, so when such a rewrite is detected and recent requests are slow, the plugin rotates for that same request: the route switch and the rewrite share one unavoidable cache miss. The last fold's cache condition is ignored in this path because its hit rate cannot be healthy after a rewrite; the performance condition still applies.
|
|
20
|
+
|
|
21
|
+
The plugin does not compress anything itself and does not change the prompt or the conversation log. It only changes one routing header and keeps local timing counters.
|
|
22
|
+
|
|
23
|
+
## How it works
|
|
24
|
+
|
|
25
|
+
- **Cache evidence.** It reads billion-context's per-session state file (`sessions/openai/<host>_<sha256(conversationId).slice(0,24)>.json`) and looks at `metadata.cacheLedger.folds`. billion-context derives `conversationId` from the highest-priority conversation header. In a DSH setup that is `x-bili-plugin-conversation` (the DSH session id); `x-opencode-session` ranks below it, so rotating that header does **not** change the file or open a new one. A fold record's `hPct` is the cache-hit percentage of the first request sent after that rebuild.
|
|
26
|
+
- **Eligibility.** Only requests from a configured provider (`opencode-go` by default) whose model id starts with `deepseek` (case-insensitive) are considered. `models` can narrow this to exact DeepSeek ids. Other models are ignored, and a rotation is skipped if the latest ordinary conversation request used a non-eligible model. Auxiliary requests that declare a `purpose` do not change that state.
|
|
27
|
+
- **Performance evidence.** It wraps `llm/stream` only for eligible requests, timestamps the first content chunk, and records output tokens per second at the finish chunk. It keeps the last `sampleSize` valid samples per DSH session.
|
|
28
|
+
- **Decision.** On a new fold, it evaluates the folds *before* that fold. Rotation happens only when the latest `foldsWindow` fold(s) all have `hPct <= cacheHitThresholdPct` **and** the recent samples are slow (average TTFT `>=` 12000 ms by default, or average throughput `<=` 60 tokens/second by default).
|
|
29
|
+
- **System-prompt rewrite.** From `llm/stream` it extracts the initial system prompt the same way pi-ai does (`options.system` plus leading `system` messages) and fingerprints it per session. When the fingerprint changes, it writes a route before the request headers are assembled, so the same request that carries the rewrite also carries the new routing id. This path uses only the performance condition (the fold cache condition is bypassed); the route records `reason: "system-rewrite"`. A later fold caused by that rewrite is guarded from rotating a second time for the same miss.
|
|
30
|
+
- **Routing.** A persisted route maps the DSH session id to a generated `ses_...` value. The `llm-pi-ai/request-headers` waterfall runs after pi-ai assembles its header set and before the request is sent, so setting `x-opencode-session` there reaches the wire. In the reference setup a companion injector such as `dsh-opencode-ip-guard` supplies a baseline value equal to the DSH session id; when a route exists this plugin replaces it, and the injector's no-overwrite fallback leaves the value alone.
|
|
31
|
+
- **No billion-context, no effect.** If the sessions directory or the session state file is absent, the monitor stays idle. The header hook has no route and does nothing. No exception is thrown into DSH.
|
|
32
|
+
|
|
33
|
+
The first fold seen after a plugin start is a baseline and never causes rotation. Rotation happens on a later fold, matching "at the next fold rebuild". The first system prompt seen in a session is only a fingerprint baseline; a rewrite rotation requires a subsequent observed change. The current session is considered eligible only while its latest ordinary conversation request is an OpenCode Go DeepSeek-series request.
|
|
34
|
+
|
|
35
|
+
A rotation changes only the upstream routing id. billion-context keeps writing the same state file for the DSH session, so the monitor keeps following it; after recording the fold that triggered the rotation it advances its fold cursor. The same rebuild cannot rotate twice, but a later rebuild can rotate again.
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- DSH with the `llm-pi-ai` adapter and an OpenCode Go provider (default `opencode-go`) configured with at least one DeepSeek-series model (model id starts with `deepseek`).
|
|
40
|
+
- billion-context installed and actively saving session state.
|
|
41
|
+
- DSH 0.2.1-rc.5 or newer, below 0.3.0.
|
|
42
|
+
- Node.js 20 or newer.
|
|
43
|
+
|
|
44
|
+
## Install from npm
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
dsh plugin add dsh-opencode-go-router --profile <profile>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or add `dsh-opencode-go-router` from the DSH plugin manager's add-plugin box. The package declares `dsh.bundle.patch`, so a normal install creates the `opencode-router` row; do not add a separate profile row for it.
|
|
51
|
+
|
|
52
|
+
## Install from a local checkout (development)
|
|
53
|
+
|
|
54
|
+
1. Keep this repository somewhere stable, for example `C:/Users/me/DSHs/opencode-router`.
|
|
55
|
+
2. In the profile's `package.json`:
|
|
56
|
+
- add `dsh-opencode-go-router` to `dsh.profile.bundles`;
|
|
57
|
+
- add `"dsh-opencode-go-router": "link:C:/Users/me/DSHs/opencode-router"` to `dependencies`.
|
|
58
|
+
3. Make the profile's `node_modules/dsh-opencode-go-router` point at the repository (a directory junction/symlink is enough on Windows).
|
|
59
|
+
4. Do **not** add a standalone row for this plugin to the profile's `cordis.patch.yml`. The package declares `dsh.bundle.patch: ./cordis.patch.yml`, which inserts the plugin row itself. A profile row added without an `insert` is only an override and cannot create the row.
|
|
60
|
+
5. Restart DSH. In the plugin manager, `dsh-opencode-go-router` should appear as a profile bundle with one row (`include:opencode-router`). If the host was already running before the bundle declaration existed, reload it through the plugin manager or restart DSH.
|
|
61
|
+
6. Read the live state at `http://127.0.0.1:<DSH-web-port>/opencode-router/status`.
|
|
62
|
+
|
|
63
|
+
To change defaults, add an override by row id to the profile's `cordis.patch.yml`:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
- id: opencode-router
|
|
67
|
+
config:
|
|
68
|
+
cacheHitThresholdPct: 25
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Because the plugin entry point reloads `lib/impl.js` with a cache-busting query on every `apply`, editing the implementation and touching the plugin config is usually enough to try new code. In a running host, toggling the profile bundle off and on through the plugin manager also reloads the static imports (`lib/core.js`, `lib/store.js`, `lib/monitor.js`); the 0.1.2 -> 0.1.3 threshold change was verified that way. If the host keeps reporting the old version, restart DSH.
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
| Key | Default | Meaning |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| `enabled` | `true` | Master switch. When false, hooks pass through and the monitor does not run. |
|
|
78
|
+
| `providers` | `["opencode-go"]` | Providers eligible for routing; the model must still be a `deepseek*` id. |
|
|
79
|
+
| `models` | `[]` | Optional exact DeepSeek model allow-list; empty means any `deepseek*` model of the provider. |
|
|
80
|
+
| `cacheHitThresholdPct` | `25` | A fold counts as "cache lost" when `hPct <= this`. Not applied to a system-prompt rewrite rotation. |
|
|
81
|
+
| `systemRewriteBypassCache` | `true` | Rotate on a system-prompt change even when the last fold's cache hit is healthy; the performance condition must still be slow. |
|
|
82
|
+
| `foldsWindow` | `1` | How many recent folds must all be below the cache threshold. |
|
|
83
|
+
| `ttftThresholdMs` | `12000` | Average time to first content token at or above this is slow. |
|
|
84
|
+
| `throughputThresholdTps` | `60` | Average output tokens/second at or below this is slow. |
|
|
85
|
+
| `sampleSize` | `5` | Number of recent requests used for the performance decision. |
|
|
86
|
+
| `requireFullSamples` | `true` | Require all `sampleSize` samples before rotating. |
|
|
87
|
+
| `routingIdPrefix` | `"ses_"` | Prefix of generated routing ids. |
|
|
88
|
+
| `statePath` | `$DSH_HOME/storages/opencode-router/state.json` | Persisted routes and timing samples. |
|
|
89
|
+
| `dryRun` | `false` | Log rotations without selecting the new header value. |
|
|
90
|
+
| `debug` | `false` | Extra info logs. |
|
|
91
|
+
| `overrideExistingHeader` | `true` | Replace an already present `x-opencode-session`. Set false to preserve it. |
|
|
92
|
+
| `pollIntervalMs` | `2000` | Poll interval while a session is active. |
|
|
93
|
+
| `requestCheckDelaysMs` | `[400, 1500]` | Extra state-file checks after a request finishes. |
|
|
94
|
+
| `biliSessionsDir` | `null` | Override billion-context's sessions root. |
|
|
95
|
+
| `biliProtocol` | `"openai"` | State subdirectory. |
|
|
96
|
+
| `biliUpstreamHost` | `"opencode.ai"` | Host label used in the state filename. |
|
|
97
|
+
| `maxSamples` | `20` | Maximum samples kept per session. |
|
|
98
|
+
| `sessionActiveMs` | `600000` | Stop polling a session after this idle time. |
|
|
99
|
+
| `minRotationGapMs` | `0` | Minimum time between rotations for one session. |
|
|
100
|
+
|
|
101
|
+
## Status route
|
|
102
|
+
|
|
103
|
+
`GET /opencode-router/status` returns JSON with the effective config, persisted routes, and per-session monitor state:
|
|
104
|
+
|
|
105
|
+
- `eligibility` — the configured providers and the fixed `deepseek` model prefix;
|
|
106
|
+
- `baselineReady` and `lastFoldSeq` — fold cursor;
|
|
107
|
+
- `latestFold` — latest fold record (including `hPct`);
|
|
108
|
+
- `watchingId` — DSH session id whose billion-context state file is being followed;
|
|
109
|
+
- `route` — current rotated routing id, or `null`; its `reason` is `cache-and-performance` or `system-rewrite`;
|
|
110
|
+
- `targetActive` — whether the latest ordinary conversation request was an eligible OpenCode Go DeepSeek request; `false` blocks rotation;
|
|
111
|
+
- `systemPromptFp` — fingerprint of the latest observed initial system prompt, or `null`;
|
|
112
|
+
- `systemRewriteBypassCache`, `systemRewriteRotations`, `lastSystemRewriteAt` — rewrite-rotation settings and counters;
|
|
113
|
+
- `samples` — count, average TTFT, average throughput;
|
|
114
|
+
- `rotations`, `lastRequestAt`, `lastTickAt`, `lastError`;
|
|
115
|
+
- `routing` — header-hook counters and the latest decision: `checks`, `applications`, `lastCheckedAt`, `lastCheckedSessionId`, `lastAppliedAt`, `lastAppliedSessionId`, `lastAppliedValue`, `lastReason`. `lastReason` is `applied`, `no-route`, `existing-header`, `no-headers`, `disabled`, or `not-target`.
|
|
116
|
+
|
|
117
|
+
The route is localhost-only when the host's web server is bound to localhost. Treat the JSON as diagnostics.
|
|
118
|
+
|
|
119
|
+
## Privacy and safety
|
|
120
|
+
|
|
121
|
+
- Stored data is limited to DSH session ids, generated routing ids, fold numbers, cache percentages, TTFT, throughput, and timestamps. No prompts, messages, tool output, or API keys are stored.
|
|
122
|
+
- Every host callback catches its own errors. A missing or malformed billion-context state file only produces a log entry.
|
|
123
|
+
- Rotating a header is a routing hint, not a guarantee that the gateway picks a different physical backend. If the gateway ignores unknown or new session values, the plugin cannot force a move.
|
|
124
|
+
- billion-context's state file is an internal format. If its layout changes, the monitor may see no folds and stay idle; the plugin will not corrupt anything.
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
node --test
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The suite covers fold parsing, the cache/performance decision, system-prompt extraction/fingerprinting, the system-rewrite decision, DeepSeek-only eligibility, state persistence, the monitor state machine, header routing, and stream measurement.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# dsh-opencode-go-router
|
|
2
|
+
|
|
3
|
+
[](https://github.com/TLEWA/dsh-opencode-go-router/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-opencode-go-router)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
[简体中文](./README.zh-CN.md) · [English](./README.md)
|
|
8
|
+
|
|
9
|
+
一个 DeepSeek Harness(DSH)宿主插件:当使用 OpenCode Go 的 DeepSeek 系列模型的会话,在 billion-context 压缩上下文后丢失了可复用的前缀缓存,**并且**最近几次请求很慢时,在**下一次压缩重建**时给 OpenCode Go 会话换一个新的路由 id;当系统提示词改写即将使下一次缓存未命中不可避免时,也会为携带改写的同一个请求换路由。
|
|
10
|
+
|
|
11
|
+
这是内部插件 id 为 `opencode-router` 的 DSH 插件的 npm 包。它只轮换 OpenCode Go 的路由请求头,不是模型适配器。
|
|
12
|
+
|
|
13
|
+
## 为什么需要它
|
|
14
|
+
|
|
15
|
+
OpenCode Go 通过请求头 `x-opencode-session` 把一条会话固定到一个上游后端。billion-context 重建上下文后,重建后的第一次请求本应在同一个后端重新预热前缀;如果这次请求的缓存命中率偏低(默认 ≤25%),同时最近几次请求又很慢,说明当前后端对该会话很可能不合适。插件会在**下一次**折叠重建时轮换 `x-opencode-session`,让后续请求换一个后端。
|
|
16
|
+
|
|
17
|
+
只有 OpenCode Go 的 DeepSeek 系列会话会被处理;其他模型即使已有路由值,也不会被本插件改写。
|
|
18
|
+
|
|
19
|
+
系统提示词改写本身就会使可复用前缀失效。携带改写的请求无论发到哪个后端都是冷请求,因此检测到这种改写且最近请求很慢时,插件会为同一个请求轮换:把换路由的冷请求和改写本身的冷请求合并为一次不可避免的未命中。这条路径忽略上一次折叠的缓存条件(改写后的命中率本来就不会健康),但仍要求性能条件成立。
|
|
20
|
+
|
|
21
|
+
插件本身不压缩上下文,也不修改提示词或会话日志;它只改一个路由请求头,并在本地记录耗时统计。
|
|
22
|
+
|
|
23
|
+
## 工作方式
|
|
24
|
+
|
|
25
|
+
- **缓存证据。** 读取 billion-context 的会话状态文件(`sessions/openai/<host>_<sha256(conversationId).slice(0,24)>.json`)里的 `metadata.cacheLedger.folds`。billion-context 按优先级最高的会话请求头推导 `conversationId`:DSH 场景下是 `x-bili-plugin-conversation`(即 DSH 会话 id),`x-opencode-session` 的优先级更低,所以轮换它**不会**改变这个文件,也不会新开文件。每条折叠记录的 `hPct` 就是该次压缩重建后第一条请求的缓存命中率。
|
|
26
|
+
- **资格。** 只处理配置的 provider(默认 `opencode-go`)中模型 id 以 `deepseek` 开头(不区分大小写)的请求;`models` 可进一步收窄到具体 DeepSeek 模型。其他模型会被忽略;如果会话最近一次普通对话请求不是合格模型,也会跳过轮换。带 `purpose` 的辅助请求不会改变这个状态。
|
|
27
|
+
- **性能证据。** 只包裹合格请求的 `llm/stream`,记录首个内容 chunk 的时间,并在 finish chunk 计算输出 tok/s;每个 DSH 会话保留最近 `sampleSize` 条有效样本。
|
|
28
|
+
- **判定。** 出现新折叠时,只评估**这次新折叠之前**的折叠。最近 `foldsWindow` 次折叠的 `hPct` 都 `<=` 阈值,**并且**最近样本的平均 TTFT `>=` 阈值(默认 12000ms)或平均吞吐 `<=` 阈值(默认 60 tok/s),才轮换。
|
|
29
|
+
- **系统提示词改写。** 在 `llm/stream` 中按 pi-ai 的方式提取初始系统提示词(`options.system` 加上开头的 `system` 消息)并按会话计算指纹。指纹变化时,在请求头组装之前写入路由,使携带改写的同一个请求也携带新路由 id。该路径只使用性能条件(绕过折叠缓存条件),路由记录 `reason: "system-rewrite"`。这次改写随后引发的折叠会被保护,不会为同一次未命中二次轮换。
|
|
30
|
+
- **路由。** 持久化 "DSH 会话 id -> 生成的 `ses_...` 值" 的映射;`llm-pi-ai/request-headers` 瀑布在 pi-ai 组装完请求头之后、真正发请求之前执行,所以在这里设置 `x-opencode-session` 能到达线上请求。参考配置里由伴生插件(如 `dsh-opencode-ip-guard`)提供兜底值(等于 DSH 会话 id);有路由时本插件覆盖它,伴生插件的"已有头不覆盖"规则则保证原值不被二次改动。
|
|
31
|
+
- **没有 billion-context 也能安全存在。** 找不到 sessions 目录或会话状态文件时,监控器保持空闲;请求头钩子没有可用路由时什么也不做;不会向 DSH 抛异常。
|
|
32
|
+
|
|
33
|
+
插件启动后看到的第一次折叠只作为基线,绝不触发轮换;轮换发生在之后的折叠,符合"在下一次折叠重建时轮换"。会话中第一次看到的系统提示词只作为指纹基线;改写轮换需要之后观察到实际变化。只有会话最近一次普通对话请求仍是 OpenCode Go 的 DeepSeek 系列请求时,该会话才处于合格状态。
|
|
34
|
+
|
|
35
|
+
轮换只改变上游路由 id。billion-context 仍在同一个状态文件里记录这个 DSH 会话,监控器继续跟踪该文件;记录完触发轮换的那次折叠后,它会把折叠游标推进到该处。同一次重建不会触发两次轮换,但之后的另一次重建仍可再次轮换。
|
|
36
|
+
|
|
37
|
+
## 依赖
|
|
38
|
+
|
|
39
|
+
- DSH 使用 `llm-pi-ai` 适配器,并配置了 OpenCode Go provider(默认 `opencode-go`),且至少配置一个 DeepSeek 系列模型(模型 id 以 `deepseek` 开头)。
|
|
40
|
+
- 已安装并正在写会话状态的 billion-context。
|
|
41
|
+
- DSH 0.2.1-rc.5 及以上、0.3.0 以下。
|
|
42
|
+
- Node.js 20 及以上。
|
|
43
|
+
|
|
44
|
+
## 从 npm 安装
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
dsh plugin add dsh-opencode-go-router --profile <profile>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
也可以在 DSH 插件管理器的「添加插件」框里填入 `dsh-opencode-go-router`。包内声明了 `dsh.bundle.patch`,正常安装会创建 `opencode-router` 行;不要再往 profile 里手动加一条独立行。
|
|
51
|
+
|
|
52
|
+
## 从本地仓库安装(开发)
|
|
53
|
+
|
|
54
|
+
1. 把本仓库放在稳定位置,例如 `C:/Users/me/DSHs/opencode-router`。
|
|
55
|
+
2. 在 profile 的 `package.json` 里:
|
|
56
|
+
- 把 `dsh-opencode-go-router` 加入 `dsh.profile.bundles`;
|
|
57
|
+
- 在 `dependencies` 里加入 `"dsh-opencode-go-router": "link:C:/Users/me/DSHs/opencode-router"`。
|
|
58
|
+
3. 让 profile 的 `node_modules/dsh-opencode-go-router` 指向本仓库(Windows 下目录 junction 或 symlink 均可)。
|
|
59
|
+
4. **不要**再往 profile 的 `cordis.patch.yml` 里手动加本插件的独立行。包内已声明 `dsh.bundle.patch: ./cordis.patch.yml`,由它插入插件行;不带 `insert` 的 profile 行只是覆盖项,创建不了插件行。
|
|
60
|
+
5. 重启 DSH。插件管理器里 `dsh-opencode-go-router` 应显示为 profile bundle,并有一行 `include:opencode-router`。如果宿主在 bundle 声明存在之前就已经启动,用插件管理器重新加载,或重启 DSH。
|
|
61
|
+
6. 访问 `http://127.0.0.1:<DSH Web 端口>/opencode-router/status` 查看实时状态。
|
|
62
|
+
|
|
63
|
+
要改默认值,在 profile 的 `cordis.patch.yml` 里按行 id 覆盖:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
- id: opencode-router
|
|
67
|
+
config:
|
|
68
|
+
cacheHitThresholdPct: 25
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
插件入口在每次 `apply` 时用带时间戳的查询串重新加载 `lib/impl.js`,所以改实现并碰一下插件配置通常就能生效。运行中的宿主里,用插件管理器把 profile bundle 关掉再打开,也会重新加载静态 import(`lib/core.js`、`lib/store.js`、`lib/monitor.js`);0.1.2 -> 0.1.3 的阈值改动就是这样验证生效的。如果状态仍显示旧版本,再完整重启 DSH。
|
|
72
|
+
|
|
73
|
+
## 配置项
|
|
74
|
+
|
|
75
|
+
| 键 | 默认值 | 含义 |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| `enabled` | `true` | 总开关;false 时钩子直接透传,监控器不启动。 |
|
|
78
|
+
| `providers` | `["opencode-go"]` | 参与路由的 provider;模型仍必须是 `deepseek*`。 |
|
|
79
|
+
| `models` | `[]` | 可选的 DeepSeek 精确模型白名单;空表示该 provider 的任意 `deepseek*` 模型。 |
|
|
80
|
+
| `cacheHitThresholdPct` | `25` | 折叠的 `hPct <= 该值` 视为丢缓存;系统提示词改写轮换不适用。 |
|
|
81
|
+
| `systemRewriteBypassCache` | `true` | 系统提示词变化时,即使上一次折叠的缓存命中健康也轮换;仍要求性能变慢。 |
|
|
82
|
+
| `foldsWindow` | `1` | 最近多少次折叠都必须低于缓存阈值。 |
|
|
83
|
+
| `ttftThresholdMs` | `12000` | 平均首内容 token 时间达到该值算慢。 |
|
|
84
|
+
| `throughputThresholdTps` | `60` | 平均输出 tok/s 低于等于该值算慢。 |
|
|
85
|
+
| `sampleSize` | `5` | 性能判定使用的最近请求数。 |
|
|
86
|
+
| `requireFullSamples` | `true` | 必须凑满 `sampleSize` 条样本才轮换。 |
|
|
87
|
+
| `routingIdPrefix` | `"ses_"` | 生成路由 id 的前缀。 |
|
|
88
|
+
| `statePath` | `$DSH_HOME/storages/opencode-router/state.json` | 持久化路由与耗时样本。 |
|
|
89
|
+
| `dryRun` | `false` | 只记录轮换日志,不真正改用新请求头。 |
|
|
90
|
+
| `debug` | `false` | 额外 info 日志。 |
|
|
91
|
+
| `overrideExistingHeader` | `true` | 是否覆盖已存在的 `x-opencode-session`;false 时保留原值。 |
|
|
92
|
+
| `pollIntervalMs` | `2000` | 会话活跃时的轮询间隔。 |
|
|
93
|
+
| `requestCheckDelaysMs` | `[400, 1500]` | 请求结束后额外检查状态文件的时间点。 |
|
|
94
|
+
| `biliSessionsDir` | `null` | 覆盖 billion-context 的 sessions 根目录。 |
|
|
95
|
+
| `biliProtocol` | `"openai"` | 状态文件子目录。 |
|
|
96
|
+
| `biliUpstreamHost` | `"opencode.ai"` | 状态文件名里的 host 标签。 |
|
|
97
|
+
| `maxSamples` | `20` | 每个会话最多保留的样本数。 |
|
|
98
|
+
| `sessionActiveMs` | `600000` | 会话空闲超过该时长后停止轮询。 |
|
|
99
|
+
| `minRotationGapMs` | `0` | 同一会话两次轮换的最小间隔。 |
|
|
100
|
+
|
|
101
|
+
## 状态路由
|
|
102
|
+
|
|
103
|
+
`GET /opencode-router/status` 返回 JSON:生效配置、已持久化路由、每个会话的监控状态:
|
|
104
|
+
|
|
105
|
+
- `eligibility` —— 配置的 provider 与固定的 `deepseek` 模型前缀;
|
|
106
|
+
- `baselineReady` 与 `lastFoldSeq` —— 折叠游标;
|
|
107
|
+
- `latestFold` —— 最新折叠记录(含 `hPct`);
|
|
108
|
+
- `watchingId` —— 当前跟踪的 billion-context 状态文件所属的 DSH 会话 id;
|
|
109
|
+
- `route` —— 当前轮换后的路由 id,或 `null`;其 `reason` 为 `cache-and-performance` 或 `system-rewrite`;
|
|
110
|
+
- `targetActive` —— 会话最近一次普通对话请求是否为合格的 OpenCode Go DeepSeek 请求;false 时禁止轮换;
|
|
111
|
+
- `systemPromptFp` —— 最近观察到的初始系统提示词指纹,或 `null`;
|
|
112
|
+
- `systemRewriteBypassCache`、`systemRewriteRotations`、`lastSystemRewriteAt` —— 改写轮换的配置与计数;
|
|
113
|
+
- `samples` —— 样本数、平均 TTFT、平均吞吐;
|
|
114
|
+
- `rotations`、`lastRequestAt`、`lastTickAt`、`lastError`;
|
|
115
|
+
- `routing` —— 请求头钩子的计数与最近一次判定:`checks`、`applications`、`lastCheckedAt`、`lastCheckedSessionId`、`lastAppliedAt`、`lastAppliedSessionId`、`lastAppliedValue`、`lastReason`。`lastReason` 取值为 `applied`、`no-route`、`existing-header`、`no-headers`、`disabled` 或 `not-target`。
|
|
116
|
+
|
|
117
|
+
如果宿主 Web 服务只监听 localhost,该路由也只在 localhost 可达。JSON 仅供诊断。
|
|
118
|
+
|
|
119
|
+
## 隐私与安全
|
|
120
|
+
|
|
121
|
+
- 只保存 DSH 会话 id、生成的路由 id、折叠序号、缓存百分比、TTFT、吞吐和时间戳;不保存提示词、消息、工具输出或 API key。
|
|
122
|
+
- 所有宿主回调都自行捕获异常。billion-context 状态文件缺失或损坏只会产生日志。
|
|
123
|
+
- 轮换请求头只是路由提示,不保证网关一定换到另一台物理后端。如果网关忽略新的会话值,插件无法强制迁移。
|
|
124
|
+
- billion-context 的状态文件是内部格式。若其结构变化,监控器可能看不到折叠并保持空闲,但不会破坏任何数据。
|
|
125
|
+
|
|
126
|
+
## 开发
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
node --test
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
测试覆盖折叠解析、缓存/性能判定、系统提示词提取与指纹、系统提示词改写判定、仅 DeepSeek 资格、状态持久化、监控状态机、请求头路由和流式耗时测量。
|
|
133
|
+
|
|
134
|
+
## 许可证
|
|
135
|
+
|
|
136
|
+
MIT
|