Tools
Plugin Model Router
Public Mirror of openclaw-plugin-model-router
Install
npm install
npm
Configuration Example
...(options?.reasoning === "off" ? { think: false } : {}),
README
# Model Router (OpenClaw plugin)
Classify each incoming turn with a small, fast local model, then:
1. **Route** the turn to the best specialist model for that category
(`before_model_resolve` → `modelOverride`).
2. **Narrow** the tool surface for that turn to only the tools the category
needs (`before_prompt_build` → `toolsAllow`).
The classification result is cached per run, so both hooks share a single
classifier call. If classification fails or times out, the turn falls back to
the session's default model and the default tool surface (fail-open).
## Why this shape
- **One small classifier, many specialists.** A 2B model is cheap and fast;
it only has to pick a label, not do the work.
- **Per-turn tool narrowing** shrinks the prompt for weaker models (e.g. a
local coder) without permanently stripping tools from any agent — this is
more precise than the `localModelLean` flag, which is all-or-nothing per
agent.
- **Fail-open.** A classifier hiccup never blocks a turn; you just get the
default model.
## Install (local dev)
```bash
cd openclaw-plugin-model-router
npm install
npm run build
openclaw plugins install --link . --force
openclaw plugins enable model-router
```
Then grant conversation-hook access (required for non-bundled plugins using
`before_model_resolve` / `before_prompt_build`), grant LLM model-override trust
(required for the classifier call), and add your routing config — see the config
block below.
## Config block (paste into `openclaw.json`)
```json5
{
"plugins": {
"entries": {
"model-router": {
"enabled": true,
"hooks": {
// Required for non-bundled plugins to use conversation hooks.
"allowConversationAccess": true
},
"llm": {
// REQUIRED: the classifier calls api.runtime.llm.complete with an
// explicit model. Without this, every classification throws
// LLM_COMPLETION_NOT_AUTHORIZED and the plugin fails open to the
// default category on every turn.
"allowModelOverride": true,
// RECOMMENDED: scope the classifier to the exact model(s) you intend
// to use. Omit or set ["*"] only if you intentionally want any model.
// Keep this list tight — it is the trust boundary for where prompts go.
"allowedModels": ["ollama/qwen3.5:2b"]
},
"config": {
"enabled": true,
// Optional: verbose per-turn diagnostics (classifier completion
// snapshot + raw classifier output). These can contain prompt-derived
// text, so they are OFF by default — enable only while troubleshooting.
// "debug": true,
// Small, fast classifier (local Ollama 2B).
"classifierModel": "ollama/qwen3.5:2b",
"classifierTimeoutMs": 3000,
// Optional: model to use when classification fails/times out.
// Omit to keep the session's default model.
// "fallbackModel": "ollama/qwen3.8:27b",
// One section per agent, like the main openclaw config's per-agent
// sections. Only agents with a section are routed; every other
// agent is skipped entirely (no classifier call, no model override,
// no tool cap).
"agents": {
"main": {
// category -> { model, toolsAllow? }
"categories": {
"chat": {
"model": "ollama/qwen3.5:4b",
// NOTE: on hosts where web search is a deferred tool (discovered
// via tool_search, invoked via tool_call), include those two as
// well — otherwise current-info turns lose working web search.
"toolsAllow": ["read", "web_search", "tool_search", "tool_call", "message"]
},
"code": {
"model": "ollama/qwen3-coder:30b",
"toolsAllow": ["read", "write", "edit", "exec", "apply_patch", "ls"]
},
"reasoning": {
"model": "ollama/qwen3.8:27b"
// no toolsAllow -> full default surface
},
"general": {
"model": "ollama/qwen3.8:27b"
}
},
// Optional: per-category guidance shown to the classifier.
// Keys should match configured category names.
"categoryDescriptions": {
"chat": "Casual banter, social replies, jokes, lightweight conversation.",
"code": "Writing, debugging, explaining, reviewing, or designing code.",
"reasoning": "Timeless logic, math, planning, tradeoff analysis.",
"general": "Normal assistant work using local/personal context."
},
// Optional: category used when the classifier output does not
// match a known key. Defaults to "general" when omitted.
"defaultCategory": "general"
},
// Another agent with its own routing — sections can diverge per
// agent (different models, categories, tool caps).
"johnny": {
"categories": {
"chat": { "model": "ollama/qwen3.5:4b" },
"general": { "model": "ollama/qwen3.8:27b" }
}
}
}
}
}
}
}
}
```
> **Note:** `toolsAllow` *caps* the tool surface for a turn; it cannot grant
> tools that policy already denies. Omitting `toolsAllow` for a category leaves
> the default surface unchanged. Multiple hooks' restrictions are intersected.
### Deferred tools (web search, etc.)
On hosts where a capability is exposed as a **deferred** tool — discovered with
`tool_search` and invoked through `tool_call` rather than as a directly
callable name — a `toolsAllow` list that names only the bare capability (e.g.
`web_search`) will *cap* the surface and drop the discovery/invocation pair.
The turn then loses working web search even though the model "has" it.
If you rely on per-turn tool narrowing for categories that need current info,
include the deferred-discovery tools in `toolsAllow` alongside the bare name:
```json5
"toolsAllow": ["read", "web_search", "tool_search", "tool_call", "message"]
```
If you are unsure whether a host uses direct or deferred tool exposure, the
safest option is to **omit `toolsAllow`** for that category (leaves the default
surface unchanged) rather than risk capping away a working capability.
### LLM trust (required)
The classifier is a real model call via `api.runtime.llm.complete` with an
explicit `model`. OpenClaw gates that on host-owned authorization:
- `plugins.entries.model-router.llm.allowModelOverride: true` — **required**,
or every classification fails with `LLM_COMPLETION_NOT_AUTHORIZED` and the
plugin silently falls back to `defaultCategory` (fail-open). You will see
`model-router classification failed ... LLM_COMPLETION_NOT_AUTHORIZED` in the
gateway log if this is missing.
- `plugins.entries.model-router.llm.allowedModels` — **recommended**. An
allowlist of canonical `provider/model` refs the classifier may target. Keep
it to exactly the model(s) you intend (e.g. `["ollama/qwen3.5:2b"]`). Use
`["*"]` only if you deliberately want any model; that widens where prompts
can be sent.
- `plugins.entries.model-router.llm.allowedCompletionModels` — optional, stricter
knob applied to *every* completion (including host-resolved defaults). Set it
to the same list as `allowedModels` if you want a single trust boundary.
## How it works
- Routing is configured per agent under `config.agents.<agentId>`. Each
section carries its own `categories` and optional `categoryDescriptions`, so
different agents can route differently. Hooks first look up the current hook
context `agentId`; **agents without a section are skipped entirely** — no
classifier call, no `modelOverride`, no `toolsAllow` cap. OpenClaw continues
with the model and tools selected by that agent's normal config.
- `before_model_resolve` runs before model resolution for active agents. It
classifies the prompt and returns `{ modelOverride }`.
- `before_prompt_build` runs before the prompt is assembled for active agents.
It reuses the cached classification and returns `{ toolsAllow }` when the
category defines one.
- The classifier is asked to reply with a single JSON object of the form
`{"category": "<label>"}`. Parsing is **strict**: the entire response must be
that one JSON object with a known label — surrounding prose, code fences, or
extra fields are rejected (the turn falls back to `defaultCategory`). This is
intentional: tolerant substring matching would let user prompt text steer
routing by influencing classifier output.
## Config validation
The host validates the config block against the manifest `configSchema` at
plugin load time and **refuses to start** the plugin if shape/type errors are
found (unknown top-level keys, a category missing its required `model`, a
non-string `toolsAllow` entry, a `classifierTimeoutMs` below 1000, etc.).
On top of that, the plugin runs **startup semantic validation** at registration
and logs a `model-router config: ...` warning for each problem it finds. These
are issues the JSON schema cannot express:
- **No agent sections configured** while the plugin is enabled — the plugin
is a no-op for every agent (this is now intentional: an agent with no
section gets no routing).
- **An agent section with no categories** — every turn for that agent will
silently fall back to the default model and tool surface.
- **`defaultCategory` points at a category that does not exist** (per agent)
— classification failures and unknown labels will be silently dropped instead
of routed. Keys are compared case-insensitively (the classifier output is
lowercased before lookup, so `Chat` and `chat` are the same routing target).
- **`category
... (truncated)
tools
Comments
Sign in to leave a comment