Bundle
dsh-auto-review-model-router
Route DSH's experimental auto-review safety reviewer to a cheaper model: every review call is re-targeted to a configured provider/model while the agent's own route is untouched.
- Source
- OldLigant
- License
- MIT
- Updated
- Updated 8 days ago
Readme
# dsh-auto-review-model-router
Route DSH's experimental **auto-review** safety gate to a model you choose, so a
premium agent model does not pay for its own safety reviews.
```yaml
# <profile>/cordis.patch.yml
- insert:
- id: auto-review-model-router
name: dsh-auto-review-model-router
config:
reviewModel: deepseek-official/deepseek-flash
```
With that entry every review runs on `deepseek-official/deepseek-flash` while the
agent keeps `claude-opus-4`, `gpt-6-astra`, or whatever else the session selected.
## The problem it solves
`@deepseek-ai/dsh-experimental-auto-review` reviews each gated tool call by
calling `ctx.llm.stream()` on **the session's own route**: it copies `provider`
and `model` out of the session request header
(`snapshotAutoReview()` in `packages/experimental/auto-review/src/index.ts`).
There is no `reviewModel` field in the shipped plugin, so an expensive agent
model is charged for one extra full review request per gated call.
## How it works
The plugin wraps two service methods at activation: `llm.stream` always, and
`llm.prepareCall` unless `wrapPrepareCall` is off.
1. A request is recognized as an auto-review call when it is **hand-built**
(it does not carry the agent loop's own-request mark) **and** its `system`
prompt starts with auto-review's fixed `REVIEW_POLICY` opening. Only such a
request is ever rewritten.
2. It is copied onto `reviewModel`, and the source route's `reasoningEffort` and
`maxTokens` are dropped. Both are properties of the *agent's* model: a
reasoning effort the target does not declare makes the harness throw
`UNSUPPORTED_REASONING_EFFORT`, and an inherited output cap can exceed the
target's limit. Either would fail every review closed.
3. Everything else — the review prompt, its policy system prompt, `temperature`,
and the `{"risk":…,"decision":…}` JSON protocol the gate parses — is
unchanged. Only the model that produces the verdict changes.
4. A review that fails transiently is retried on the same route
(`retryAttempts`, default 4). The gate calls `llm.stream` directly, outside
the agent loop, so the harness retry plugin (an `agent/request-error`
listener) never sees a review: without this, one dropped connection denies
the tool call.
### Why retries need their own classification
Only the codes the harness itself treats as transient are retried:
`EMPTY_RESPONSE`, `RATE_LIMIT`, `SERVER`, `TRANSPORT`. `TIMEOUT` is deliberately
excluded — the DeepSeek adapter raises it from an **idle watchdog**, so retrying
it multiplies a silent wait by the attempt count while the auto-review deadline
drains. `ABORTED` (the deadline, or the caller cancelling) and configuration
errors such as `AUTH` are not fixed by trying again.
A failed attempt's terminal `finish` chunk is held back: the gate's reader ends
the review on any terminal `finish`, so yielding it would end the review on the
first transport blip. Reasoning chunks stay live (the reader ignores reasoning
content), so a retried review exposes more than one attempt's reasoning while the
verdict is still parsed from the final text block alone.
### Why the wrappers, and not an `llm/stream` listener
Two seams were tried first and both are dead ends on a real runtime:
- Mutating the request inside an `llm/stream` listener throws: auto-review
freezes it (`deepFreeze`) before streaming.
- Returning `next(replacement)` from a listener never reaches the adapter:
Cordis's `EventsService.waterfall` invokes the innermost callback with the
argument list it closed over.
- Writing `ctx.llm.stream = wrapper` is also ineffective: `ctx.llm` is a
per-read traceable proxy, and its property overlay shadows what you write.
The wrappers are therefore installed on the object that owns each method in the
service's prototype chain (`LlmRuntime.prototype` in practice), which is the one
seam every consumer actually calls. Both are removed when the plugin's fiber
disposes.
### Why `prepareCall` needs its own wrapper
A prepared handle resolves its route during `prepareCall` and then rejects a
dispatch whose config differs from the resolved one
(`INVALID_PREPARED_CALL`). The retarget must therefore happen *before* the call
is prepared; swapping the route in the returned handle cannot work. With
`wrapPrepareCall: true` (the default) a review prepared that way runs on the
review route, and turning it off restores the session route for that path.
## Install
Published on npm as `dsh-auto-review-model-router`:
```sh
# in the profile directory
pnpm add dsh-auto-review-model-router
```
From a local checkout instead, link it:
```jsonc
// <profile>/package.json
"dependencies": { "dsh-auto-review-model-router": "link:D:/path/to/dsh-auto-review-model-router" }
```
Then insert it in the profile patch layer, after the auto-review bundle:
```yaml
# <profile>/cordis.patch.yml
- insert:
- id: auto-review-model-router
name: dsh-auto-review-model-router
config:
reviewModel: deepseek-official/deepseek-flash
```
Restart the app: patch-layer changes apply at profile startup.
## Configuration
| Field | Default | Meaning |
| --- | --- | --- |
| `reviewModel` | `deepseek-official/deepseek-flash` | `<provider>/<model>` every review runs on. An empty string disables routing. |
| `systemPromptPatterns` | the shipped `REVIEW_POLICY` opening | Substrings, or `/regex/u` literals, matched against a request's `system` prompt. Widen this only if a harness release rewrites that prompt. |
| `retryAttempts` | `4` | Retries after the first attempt of one review call, so up to 5 attempts. `0` disables retrying. Maximum 10. |
| `retryDelayMs` | `500` | Pause before each retry. |
| `wrapPrepareCall` | `true` | Also route review requests issued through `llm.prepareCall`. |
| `dropReasoningEffort` | `true` | Drop the source route's reasoning effort on the review request. |
| `dropMaxTokens` | `true` | Drop the source route's output cap on the review request. |
| `logRetargets` | `true` | Log each retargeted review. Retries log a warning regardless. |
## Verify
The test suite has two files. `test/router.test.js` covers the decision surface
with no harness at all; `test/live.test.js` runs the wrappers over the real
`LlmRuntime` and skips those checks when neither this package nor
`DSH_PROFILE_DIR` resolves `@deepseek-ai/dsh-llm`.
```sh
node test/router.test.js
DSH_PROFILE_DIR=<profile dir> node test/live.test.js
```
Two commands rather than `node --test`: each file runs its `node:test` suite
in-process, while `node --test` spawns a child process per file.
> The plugin patches `LlmRuntime.prototype`, so each live test disposes its own
> mount. Skipping that teardown leaks a wrapper into the next test's dispatches.
At runtime each retarget logs one line, and each retry logs a warning:
```
auto-review-model-router: review request re-routed anthropic/claude-opus-4 -> deepseek-official/deepseek-flash (stream)
auto-review-model-router: review attempt 1/5 failed with TRANSPORT; retrying in 500 ms
```
## Scope and limits
- Only requests recognized as auto-review calls change route. A false negative
costs nothing (the review runs exactly as before); a false positive is bounded
to one extra object and one changed route for one hand-built request.
- The standalone `dsh-auto-review` plugin needs none of this: its `reviewerModel`
config field already sets the reviewer's model directly.
- DSH public APIs are pre-stable. A release that changes the `REVIEW_POLICY`
opening, `LlmRuntime.prototype`'s method names, or the prepared-handle contract
needs a matching update here. Detection is configurable
(`systemPromptPatterns`) precisely for the first case, and the plugin logs a
warning instead of failing the profile boot when a method it needs is missing.
## License
MIT
Install
dsh plugin --profile web add github:OldLigant/dsh-auto-review-model-router
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-auto-review-model-router from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.