qwen-agent-server 0.11.1
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/README.md +211 -0
- package/dist/backends.js +444 -0
- package/dist/backends.js.map +1 -0
- package/dist/embed.js +92 -0
- package/dist/embed.js.map +1 -0
- package/dist/extensions.js +497 -0
- package/dist/extensions.js.map +1 -0
- package/dist/log.js +21 -0
- package/dist/log.js.map +1 -0
- package/dist/openai-compat.js +147 -0
- package/dist/openai-compat.js.map +1 -0
- package/dist/permissions.js +71 -0
- package/dist/permissions.js.map +1 -0
- package/dist/pool.js +155 -0
- package/dist/pool.js.map +1 -0
- package/dist/rerank.js +93 -0
- package/dist/rerank.js.map +1 -0
- package/dist/server.js +1050 -0
- package/dist/server.js.map +1 -0
- package/dist/session.js +649 -0
- package/dist/session.js.map +1 -0
- package/dist/shutdown.js +68 -0
- package/dist/shutdown.js.map +1 -0
- package/dist/threads.js +218 -0
- package/dist/threads.js.map +1 -0
- package/dist/tokenize.js +90 -0
- package/dist/tokenize.js.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/dist/vision.js +293 -0
- package/dist/vision.js.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# qwen-agent-server
|
|
2
|
+
|
|
3
|
+
Stateful MCP supervisor that exposes a local Qwen Code inference stack as a
|
|
4
|
+
set of MCP tools. Stateful chat surface — `qwen_spawn`, `qwen_poll`,
|
|
5
|
+
`qwen_send`, `qwen_stop`, `qwen_sessions` — manages long-lived `@qwen-code/sdk`
|
|
6
|
+
sessions per task. Stateless single-turn — `qwen_oneshot`,
|
|
7
|
+
`qwen_oneshot_vision` — for operator-dispatch shapes. Non-chat —
|
|
8
|
+
`qwen_embed`, `qwen_rerank`, `qwen_tokenize` — POST direct to backend
|
|
9
|
+
endpoints (bypass the SDK pipeline which is text-only). Lifecycle / introspection —
|
|
10
|
+
`qwen_backends`, `qwen_extensions`, `qwen_reload_extensions`. See the top-level
|
|
11
|
+
[README's MCP tools table](../../README.md#mcp-tools) for full per-tool detail.
|
|
12
|
+
|
|
13
|
+
The server is intentionally minimal: it is a thin supervisor layer, not a
|
|
14
|
+
framework. All inference happens inside Qwen Code via the SDK; the server's
|
|
15
|
+
job is session lifecycle, backend routing, and the canUseTool permission gate.
|
|
16
|
+
See `docs/rdr/RDR-001` for the full architecture rationale.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
**Step 1 — start the inference backend**
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
./scripts/start-stack.sh
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This launches llama-server on `localhost:8080` running `qwen3.6-27b-instruct`.
|
|
29
|
+
The health endpoint at `http://localhost:8080/health` must return 200 before
|
|
30
|
+
the server can route traffic.
|
|
31
|
+
|
|
32
|
+
**Step 2 — build and install**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
./scripts/setup-qwen-agent-server.sh
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Idempotent. Runs `npm install` + `npm run build`, creates the Qwen home
|
|
39
|
+
directory (`~/.qwen-agent-server-home` by default), and prints the
|
|
40
|
+
registration command.
|
|
41
|
+
|
|
42
|
+
**Step 3 — register with Claude Code**
|
|
43
|
+
|
|
44
|
+
Copy and run the registration command printed by the setup script:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
claude mcp add --scope user qwen-agent-server \
|
|
48
|
+
"node /path/to/repo/mcp-bridges/qwen-agent-server/dist/server.js"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
After registration, `qwen_spawn`, `qwen_poll`, `qwen_send`, `qwen_stop`,
|
|
52
|
+
and `qwen_backends` appear in Claude Code's MCP tool list.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
All configuration is via environment variables passed to the server process.
|
|
59
|
+
The setup script and registration command can be prefixed with these.
|
|
60
|
+
|
|
61
|
+
| Variable | Default | Description |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `QWEN_BACKENDS` | `[{"id":"local","url":"http://localhost:8080/v1","model":"qwen3.6-27b-instruct","tier":"local","capacity":"heavy"}]` | JSON array of `Backend` objects (see `src/types.ts`). Each entry requires `id`, `url`, `model`, `tier` (`"local"` or `"remote"`), `capacity` (`"fast"` or `"heavy"`). Optional: `weight` (default 1). |
|
|
64
|
+
| `QWEN_SUPERVISOR_MAX_SESSIONS` | `3` | Maximum concurrent active sessions. `qwen_spawn` returns an error if the cap is reached. |
|
|
65
|
+
| `QWEN_SUPERVISOR_IDLE_TTL_MS` | `1800000` | Milliseconds before an idle session (no `qwen_poll` activity) is evicted. Default = 30 minutes. |
|
|
66
|
+
| `ROUTER_HEAVY_THRESHOLD_TOKENS` | `2000` | Estimated token count above which the router prefers a `capacity:heavy` backend. |
|
|
67
|
+
| `ROUTER_HEAVY_KEYWORDS` | `prove,derive,architect,design` | Comma-separated prompt keywords that trigger routing to a `capacity:heavy` backend regardless of token count. |
|
|
68
|
+
|
|
69
|
+
Example with a remote Strix Halo box (Tailscale-reachable) joined to the
|
|
70
|
+
local Mac backend:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
QWEN_BACKENDS='[
|
|
74
|
+
{"id":"local-mac","url":"http://localhost:8080/v1","model":"qwen3.6-27b-instruct","tier":"local","capacity":"fast"},
|
|
75
|
+
{"id":"strix","url":"http://your-strix-host:1234/v1","model":"qwen3.6-35b-a3b","tier":"remote","capacity":"heavy"}
|
|
76
|
+
]' \
|
|
77
|
+
claude mcp add --scope user qwen-agent-server \
|
|
78
|
+
"node /path/to/repo/mcp-bridges/qwen-agent-server/dist/server.js"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The router prefers `capacity:heavy` for prompts over
|
|
82
|
+
`ROUTER_HEAVY_THRESHOLD_TOKENS` or containing
|
|
83
|
+
`ROUTER_HEAVY_KEYWORDS`, falling back to `capacity:fast`. The `model`
|
|
84
|
+
field must match what `/v1/models` returns from each backend (for
|
|
85
|
+
llama-server it's the `--alias` value; for LM Studio it's the loaded
|
|
86
|
+
GGUF's identifier).
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Extensions
|
|
91
|
+
|
|
92
|
+
Per-spawn Qwen Code extension loadout (RDR-002). The orchestrator chooses
|
|
93
|
+
which extensions are active for each session via `qwen_spawn`'s
|
|
94
|
+
`opts.extensions` field. The SDK doesn't expose `extensions` in
|
|
95
|
+
`QueryOptions` directly — the supervisor bridges by setting
|
|
96
|
+
`pathToQwenExecutable` to a wrapper script (`scripts/qwen-extensions-wrapper.sh`)
|
|
97
|
+
that reads `QWEN_AGENT_EXTENSIONS` from env and prepends `--extensions <list>`
|
|
98
|
+
to the CLI's argv.
|
|
99
|
+
|
|
100
|
+
**Startup resolution.** The supervisor resolves the real `qwen` binary
|
|
101
|
+
once at startup. `QWEN_REAL_BIN` (env override, verified to exist and be
|
|
102
|
+
executable) takes precedence; otherwise `which qwen` is consulted. Either
|
|
103
|
+
miss is a fail-fast non-zero exit — an operator who hasn't installed Qwen
|
|
104
|
+
Code can't recover at first spawn, only by fixing the install.
|
|
105
|
+
|
|
106
|
+
**Per-spawn semantics.** `opts.extensions` accepts three optional
|
|
107
|
+
sub-fields:
|
|
108
|
+
|
|
109
|
+
| Field | Effect |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `only: ['a','b']` | Exact-set semantics. `enable` and `disable` are ignored in this branch. Empty `only: []` disables all extensions for the spawn (`--extensions none`). |
|
|
112
|
+
| `enable: ['c']` | Additively unions onto the session-default base. |
|
|
113
|
+
| `disable: ['a']` | Subtractively removes from the session-default base after `enable`. `disable` wins on overlap. |
|
|
114
|
+
|
|
115
|
+
The session-default base is `QWEN_DEFAULT_EXTENSIONS` (a comma-list) when
|
|
116
|
+
set, otherwise the CLI's defaults (all enabled per
|
|
117
|
+
`extension-enablement.json`) — in which case the wrapper drops the
|
|
118
|
+
`--extensions` flag and the CLI inherits its own behaviour. Because the
|
|
119
|
+
supervisor cannot enumerate the implicit set, `enable`/`disable` without
|
|
120
|
+
either `QWEN_DEFAULT_EXTENSIONS` or `only` is rejected with a
|
|
121
|
+
`spawn_error` envelope rather than silently producing the wrong set.
|
|
122
|
+
|
|
123
|
+
Example — pin a session to one extension:
|
|
124
|
+
|
|
125
|
+
```jsonc
|
|
126
|
+
// qwen_spawn input
|
|
127
|
+
{
|
|
128
|
+
"task": "Refactor the auth module",
|
|
129
|
+
"opts": { "extensions": { "only": ["serena"] } }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Names match `config.name` from each extension's `qwen-extension.json`,
|
|
134
|
+
case-insensitive. Resolved unknown names produce a
|
|
135
|
+
`{ error: { code: "spawn_error", message: "unknown extension(s): X" } }`
|
|
136
|
+
envelope and no session is instantiated.
|
|
137
|
+
|
|
138
|
+
**Cache + reload.** The supervisor caches the installed-extension name
|
|
139
|
+
list at startup by parsing `qwen extensions list` output. Drain semantics
|
|
140
|
+
apply: in-flight sessions retain whatever set was resolved at their spawn
|
|
141
|
+
time; cache reloads only affect future spawns. Operators who install or
|
|
142
|
+
uninstall extensions while the supervisor is running can pick up the
|
|
143
|
+
change via the `qwen_reload_extensions` MCP tool. (Pre-v0.3 this was
|
|
144
|
+
gated behind `QWEN_ADMIN_TOOLS=1`; the gate was removed when the slash-
|
|
145
|
+
command surface took over operator-facing privileged ops — the tool is
|
|
146
|
+
now registered unconditionally whenever an extensions cache is wired,
|
|
147
|
+
which is always in `main()`.) See RDR-002 §Resolution-algorithm and
|
|
148
|
+
§Installed-extensions cache for the full design.
|
|
149
|
+
|
|
150
|
+
| Variable | Default | Description |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `QWEN_REAL_BIN` | (resolved via `which qwen`) | Override for the real Qwen Code binary path. Verified at startup. |
|
|
153
|
+
| `QWEN_DEFAULT_EXTENSIONS` | unset (CLI defaults apply) | Comma-list of extension names that the supervisor uses as the session-default base when `opts.extensions.only` is unset. |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## SDK pin policy
|
|
158
|
+
|
|
159
|
+
`@qwen-code/sdk` is pinned **exact** to `0.1.7` in `package.json`. This is
|
|
160
|
+
intentional and must not be bumped without running the integration test suite
|
|
161
|
+
against a live backend.
|
|
162
|
+
|
|
163
|
+
**Why exact?** RDR-001 §Q1 documents that the deny-with-message path
|
|
164
|
+
(`{ behavior: 'deny', message: '<answer>' }` in `canUseTool`) is the proven
|
|
165
|
+
mechanism by which `ask_user_question` answers are delivered back to the model.
|
|
166
|
+
This is empirically verified (see `/tmp/qwen-sdk-probe/probe.mjs`, Spike B,
|
|
167
|
+
2026-05-04) but is not part of the SDK's public API contract. A patch or minor
|
|
168
|
+
release could silently change it.
|
|
169
|
+
|
|
170
|
+
Similarly, KV-cache affinity depends on the SDK preserving context across turns
|
|
171
|
+
within one `query()` call. The session layer pins `session.backend` at
|
|
172
|
+
construction and never reassigns it (§Q3 KV-cache affinity) — but an SDK
|
|
173
|
+
change to connection management could break cache locality invisibly.
|
|
174
|
+
|
|
175
|
+
**Gate before bumping:**
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# 1. Ensure llama-server is running
|
|
179
|
+
curl -sf http://localhost:8080/health
|
|
180
|
+
|
|
181
|
+
# 2. Run the integration suite
|
|
182
|
+
cd mcp-bridges/qwen-agent-server
|
|
183
|
+
npm run test:integration
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
If **any** of the three SDK pin assertions fail, do **not** bump the SDK.
|
|
187
|
+
File a report against RDR-001 and investigate whether the fallback paths
|
|
188
|
+
documented there cover the regression before proceeding.
|
|
189
|
+
|
|
190
|
+
The three pin tests are in
|
|
191
|
+
`tests/integration/sdk-behavior.test.ts`.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
cd mcp-bridges/qwen-agent-server
|
|
199
|
+
|
|
200
|
+
# Unit tests (no backend required)
|
|
201
|
+
npm test
|
|
202
|
+
|
|
203
|
+
# Integration tests (requires llama-server on :8080)
|
|
204
|
+
npm run test:integration
|
|
205
|
+
|
|
206
|
+
# Build
|
|
207
|
+
npm run build
|
|
208
|
+
|
|
209
|
+
# Run directly (after build)
|
|
210
|
+
node dist/server.js
|
|
211
|
+
```
|
package/dist/backends.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Backend pool, routing heuristic, and cached health probe.
|
|
4
|
+
//
|
|
5
|
+
// Pure-logic-plus-fetch — NO @qwen-code/sdk dependency. The supervisor
|
|
6
|
+
// (session.ts) consumes Backend objects from chooseBackend() and uses
|
|
7
|
+
// them to configure SDK queries; this module never imports the SDK or
|
|
8
|
+
// touches session state.
|
|
9
|
+
//
|
|
10
|
+
// See RDR-001 §Routing for the 6-step algorithm and §Q4 for cap/idle
|
|
11
|
+
// rationale (cap/idle live in server.ts; this module only routes).
|
|
12
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { createLogger } from "./log.js";
|
|
16
|
+
const log = createLogger("qwen-backends");
|
|
17
|
+
/**
|
|
18
|
+
* On-disk config file resolved at `~/.qwen-coprocessor-stack/config.json`.
|
|
19
|
+
*
|
|
20
|
+
* Supports a hot-reload pattern: callers re-invoke their reader on each
|
|
21
|
+
* spawn / health probe; we cache the parsed object by mtime and re-parse
|
|
22
|
+
* only when the file changes. Existing sessions stay pinned to their
|
|
23
|
+
* backend (RDR-001 §Q3) — only future spawns see the updated list.
|
|
24
|
+
*
|
|
25
|
+
* Schema (object form, forward-extensible):
|
|
26
|
+
*
|
|
27
|
+
* {
|
|
28
|
+
* "backends": [
|
|
29
|
+
* { "id": "...", "url": "...", "model": "...",
|
|
30
|
+
* "tier": "local" | "remote",
|
|
31
|
+
* "capacity": "fast" | "heavy",
|
|
32
|
+
* "weight": 1 }
|
|
33
|
+
* ],
|
|
34
|
+
* "default_extensions": ["serena", "context7"]
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* Resolution priorities (highest first):
|
|
38
|
+
* - backends: QWEN_BACKENDS env → config.backends → DEFAULT_BACKEND
|
|
39
|
+
* - default extensions: QWEN_DEFAULT_EXTENSIONS env → config.default_extensions → "leave-defaults"
|
|
40
|
+
*/
|
|
41
|
+
/** Default config dir; tests and operators can override via QWEN_CONFIG_DIR env var. */
|
|
42
|
+
const DEFAULT_CONFIG_DIR = join(homedir(), ".qwen-coprocessor-stack");
|
|
43
|
+
export function getConfigDir() {
|
|
44
|
+
const override = process.env["QWEN_CONFIG_DIR"];
|
|
45
|
+
return override && override.trim() !== "" ? override : DEFAULT_CONFIG_DIR;
|
|
46
|
+
}
|
|
47
|
+
export function getConfigPath() {
|
|
48
|
+
return join(getConfigDir(), "config.json");
|
|
49
|
+
}
|
|
50
|
+
let _configCache = null;
|
|
51
|
+
/** Test-only: drop the cached config so the next read re-parses. */
|
|
52
|
+
export function _resetConfigCache() {
|
|
53
|
+
_configCache = null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read the full config file, mtime-cached. Returns the parsed object on
|
|
57
|
+
* success, or null when the file doesn't exist / is unreadable / fails
|
|
58
|
+
* to parse. A non-null return doesn't imply any field is populated;
|
|
59
|
+
* consumers check the field they need.
|
|
60
|
+
*/
|
|
61
|
+
export function readConfig() {
|
|
62
|
+
const path = getConfigPath();
|
|
63
|
+
if (!existsSync(path))
|
|
64
|
+
return null;
|
|
65
|
+
let mtimeMs;
|
|
66
|
+
try {
|
|
67
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (_configCache && _configCache.mtimeMs === mtimeMs) {
|
|
73
|
+
return _configCache.parsed;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const raw = readFileSync(path, "utf8");
|
|
77
|
+
const parsed = JSON.parse(raw);
|
|
78
|
+
_configCache = { mtimeMs, parsed };
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
log.warn({ event_type: "config_invalid", path, err: err instanceof Error ? err.message : String(err) }, "config.json present but unreadable; falling through to env / default");
|
|
83
|
+
_configCache = { mtimeMs, parsed: null };
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function readConfigBackends() {
|
|
88
|
+
const cfg = readConfig();
|
|
89
|
+
if (!cfg || !Array.isArray(cfg.backends) || cfg.backends.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
return cfg.backends;
|
|
92
|
+
}
|
|
93
|
+
const DEFAULT_MAX_CONTEXT_TOKENS = 111_000;
|
|
94
|
+
const DEFAULT_MAX_TOOL_CALLS = 0;
|
|
95
|
+
const CTX_SIZE_HEADROOM = 0.85;
|
|
96
|
+
function parseNumericEnv(name, env) {
|
|
97
|
+
const raw = env[name];
|
|
98
|
+
if (raw === undefined || raw.trim() === "")
|
|
99
|
+
return null;
|
|
100
|
+
const n = Number.parseInt(raw, 10);
|
|
101
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
102
|
+
log.warn({ event_type: "config_invalid", source: "env", var: name, raw }, "env var ignored: not a non-negative integer");
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return n;
|
|
106
|
+
}
|
|
107
|
+
export function getSessionBudgetDefaults(env = process.env, backend) {
|
|
108
|
+
const cfg = readConfig();
|
|
109
|
+
const cfgBudget = cfg?.session_budget;
|
|
110
|
+
const envMaxCtx = parseNumericEnv("QWEN_MAX_CONTEXT_TOKENS", env);
|
|
111
|
+
const cfgMaxCtx = typeof cfgBudget?.max_context_tokens === "number" && cfgBudget.max_context_tokens >= 0
|
|
112
|
+
? cfgBudget.max_context_tokens
|
|
113
|
+
: null;
|
|
114
|
+
const backendDerivedCtx = backend !== undefined && typeof backend.ctx_size === "number" && backend.ctx_size > 0
|
|
115
|
+
? Math.floor(backend.ctx_size * CTX_SIZE_HEADROOM)
|
|
116
|
+
: null;
|
|
117
|
+
const envMaxCalls = parseNumericEnv("QWEN_MAX_TOOL_CALLS", env);
|
|
118
|
+
const cfgMaxCalls = typeof cfgBudget?.max_tool_calls === "number" && cfgBudget.max_tool_calls >= 0
|
|
119
|
+
? cfgBudget.max_tool_calls
|
|
120
|
+
: null;
|
|
121
|
+
return {
|
|
122
|
+
max_context_tokens: envMaxCtx ?? cfgMaxCtx ?? backendDerivedCtx ?? DEFAULT_MAX_CONTEXT_TOKENS,
|
|
123
|
+
max_tool_calls: envMaxCalls ?? cfgMaxCalls ?? DEFAULT_MAX_TOOL_CALLS,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Read `default_extensions` from the config file. Returns null when the
|
|
128
|
+
* field is unset or empty so callers can fall through to the next
|
|
129
|
+
* resolution tier.
|
|
130
|
+
*/
|
|
131
|
+
export function readConfigDefaultExtensions() {
|
|
132
|
+
const cfg = readConfig();
|
|
133
|
+
if (!cfg || !Array.isArray(cfg.default_extensions) || cfg.default_extensions.length === 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return cfg.default_extensions;
|
|
137
|
+
}
|
|
138
|
+
// ─────────────────────────────────────────────────────────────────
|
|
139
|
+
// Configuration
|
|
140
|
+
const DEFAULT_BACKEND = {
|
|
141
|
+
id: "local-27b",
|
|
142
|
+
url: "http://localhost:8080/v1",
|
|
143
|
+
model: "qwen3.6-27b-instruct",
|
|
144
|
+
tier: "local",
|
|
145
|
+
capacity: "fast",
|
|
146
|
+
};
|
|
147
|
+
const HEALTH_TTL_MS = 30_000;
|
|
148
|
+
const COLD_PROBE_TIMEOUT_MS = 2_000;
|
|
149
|
+
const HEAVY_KEYWORDS_DEFAULT = "prove,derive,architect,design";
|
|
150
|
+
const HEAVY_THRESHOLD_DEFAULT = 2_000;
|
|
151
|
+
/**
|
|
152
|
+
* Refresh `pool.backends` in-place from `loadBackends()`. Mutates the
|
|
153
|
+
* existing array reference (splice) so any callers that captured a
|
|
154
|
+
* reference at pool construction time see the new list.
|
|
155
|
+
*
|
|
156
|
+
* Safe to call on every spawn / health probe — the env read is cheap
|
|
157
|
+
* and the file read is mtime-cached. Existing sessions stay pinned to
|
|
158
|
+
* their backend (RDR-001 §Q3); only future spawns and health listings
|
|
159
|
+
* see the updated list.
|
|
160
|
+
*/
|
|
161
|
+
export function refreshPoolBackends(pool) {
|
|
162
|
+
const fresh = loadBackends();
|
|
163
|
+
pool.backends.splice(0, pool.backends.length, ...fresh);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Read the active backend list, with hot-reload semantics.
|
|
167
|
+
*
|
|
168
|
+
* Resolution priority:
|
|
169
|
+
* 1. QWEN_BACKENDS env var — back-compat / shell override
|
|
170
|
+
* 2. ~/.qwen-coprocessor-stack/config.json `backends` array
|
|
171
|
+
* 3. DEFAULT_BACKEND fallback
|
|
172
|
+
*
|
|
173
|
+
* Invalid JSON at either source is logged as a warning and the next
|
|
174
|
+
* tier is consulted. The config file is mtime-cached so re-invocation
|
|
175
|
+
* on every spawn is cheap (one stat + maybe one parse).
|
|
176
|
+
*/
|
|
177
|
+
export function loadBackends() {
|
|
178
|
+
// 1. env override
|
|
179
|
+
const raw = process.env["QWEN_BACKENDS"];
|
|
180
|
+
if (raw && raw.trim() !== "") {
|
|
181
|
+
try {
|
|
182
|
+
const parsed = JSON.parse(raw);
|
|
183
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
184
|
+
return parsed;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
log.warn({ event_type: "config_invalid", source: "env" }, "QWEN_BACKENDS is not valid JSON; falling through to config file / default");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// 2. config file
|
|
192
|
+
const fromFile = readConfigBackends();
|
|
193
|
+
if (fromFile)
|
|
194
|
+
return fromFile;
|
|
195
|
+
// 3. default
|
|
196
|
+
return [DEFAULT_BACKEND];
|
|
197
|
+
}
|
|
198
|
+
// ─────────────────────────────────────────────────────────────────
|
|
199
|
+
// Capacity classification
|
|
200
|
+
/**
|
|
201
|
+
* Approx token count via a 1.3× word-count heuristic, floored by a
|
|
202
|
+
* chars/4 estimate so whitespace-poor inputs (base64 blobs, minified
|
|
203
|
+
* code, packed JSON) don't silently classify as fast when they're
|
|
204
|
+
* actually heavy. The chars/4 floor matches the budget enforcer's
|
|
205
|
+
* estimate so routing and budgeting agree on input size.
|
|
206
|
+
* NOT tiktoken — the threshold is a routing hint, not a billing
|
|
207
|
+
* number. (Round-2 critique bead 1m4.)
|
|
208
|
+
*/
|
|
209
|
+
export function approxTokens(text) {
|
|
210
|
+
const trimmed = text?.trim() ?? "";
|
|
211
|
+
if (trimmed === "")
|
|
212
|
+
return 0;
|
|
213
|
+
const wordEstimate = trimmed.split(/\s+/).length * 1.3;
|
|
214
|
+
const charEstimate = trimmed.length / 4;
|
|
215
|
+
return Math.round(Math.max(wordEstimate, charEstimate));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Classify a prompt into 'fast' or 'heavy'. Heavy if either:
|
|
219
|
+
* - approx token count ≥ ROUTER_HEAVY_THRESHOLD_TOKENS (default 2000), or
|
|
220
|
+
* - prompt matches any keyword in ROUTER_HEAVY_KEYWORDS (default
|
|
221
|
+
* "prove,derive,architect,design"); whole-word case-insensitive.
|
|
222
|
+
*/
|
|
223
|
+
export function classifyCapacity(prompt) {
|
|
224
|
+
const threshold = parseInt(process.env["ROUTER_HEAVY_THRESHOLD_TOKENS"] ?? String(HEAVY_THRESHOLD_DEFAULT), 10);
|
|
225
|
+
if (approxTokens(prompt) >= threshold)
|
|
226
|
+
return "heavy";
|
|
227
|
+
const kwRaw = process.env["ROUTER_HEAVY_KEYWORDS"] ?? HEAVY_KEYWORDS_DEFAULT;
|
|
228
|
+
const keywords = kwRaw.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
229
|
+
if (keywords.length === 0)
|
|
230
|
+
return "fast";
|
|
231
|
+
const lower = prompt.toLowerCase();
|
|
232
|
+
for (const kw of keywords) {
|
|
233
|
+
const re = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
234
|
+
if (re.test(lower))
|
|
235
|
+
return "heavy";
|
|
236
|
+
}
|
|
237
|
+
return "fast";
|
|
238
|
+
}
|
|
239
|
+
const healthCache = new Map();
|
|
240
|
+
const refreshInFlight = new Set();
|
|
241
|
+
/** Test-only helper to clear all health state. */
|
|
242
|
+
export function resetHealthCache() {
|
|
243
|
+
healthCache.clear();
|
|
244
|
+
refreshInFlight.clear();
|
|
245
|
+
rrCounters.clear();
|
|
246
|
+
}
|
|
247
|
+
/** Fire one /health probe (or /v1/models fallback) with a hard timeout.
|
|
248
|
+
*
|
|
249
|
+
* llama-server exposes /health at the host root (NOT under /v1), so we
|
|
250
|
+
* derive a host base by stripping the /v1 suffix. The OpenAI-compat
|
|
251
|
+
* /v1/models endpoint is the secondary probe — works across more
|
|
252
|
+
* backends but is heavier than /health.
|
|
253
|
+
*/
|
|
254
|
+
export async function probeHealth(backend) {
|
|
255
|
+
const probeUrl = async (url, timeoutMs) => {
|
|
256
|
+
const ac = new AbortController();
|
|
257
|
+
const t = setTimeout(() => ac.abort(), timeoutMs);
|
|
258
|
+
try {
|
|
259
|
+
const r = await fetch(url, { method: "GET", signal: ac.signal });
|
|
260
|
+
return r.ok;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
clearTimeout(t);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const baseUrl = stripTrailingSlash(backend.url);
|
|
270
|
+
const hostBase = baseUrl.replace(/\/v1$/, "");
|
|
271
|
+
// Prefer llama-server /health at the host root.
|
|
272
|
+
if (await probeUrl(`${hostBase}/health`, COLD_PROBE_TIMEOUT_MS))
|
|
273
|
+
return true;
|
|
274
|
+
// Fall back to OpenAI-compat /v1/models — universally available on any
|
|
275
|
+
// OpenAI-shaped backend.
|
|
276
|
+
if (await probeUrl(`${baseUrl}/models`, COLD_PROBE_TIMEOUT_MS))
|
|
277
|
+
return true;
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
function stripTrailingSlash(s) {
|
|
281
|
+
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Cache-aware health lookup.
|
|
285
|
+
*
|
|
286
|
+
* - Fresh cache (within TTL): return synchronously.
|
|
287
|
+
* - Stale cache: return cached value, kick off a background refresh.
|
|
288
|
+
* - No cache: SYNC PROBE with 2s timeout, store result. On timeout, store
|
|
289
|
+
* `null` so the next call re-probes (rather than caching false and
|
|
290
|
+
* refusing to ever try again).
|
|
291
|
+
*
|
|
292
|
+
* "null" is treated as healthy by chooseBackend (optimistic) so unprobed
|
|
293
|
+
* backends aren't permanently excluded.
|
|
294
|
+
*/
|
|
295
|
+
export async function getCachedHealth(backend) {
|
|
296
|
+
const now = Date.now();
|
|
297
|
+
const cached = healthCache.get(backend.id);
|
|
298
|
+
if (cached && now - cached.probed_at < HEALTH_TTL_MS) {
|
|
299
|
+
return cached.healthy;
|
|
300
|
+
}
|
|
301
|
+
if (cached) {
|
|
302
|
+
// Stale — return current value, refresh in background
|
|
303
|
+
if (!refreshInFlight.has(backend.id)) {
|
|
304
|
+
refreshInFlight.add(backend.id);
|
|
305
|
+
void (async () => {
|
|
306
|
+
try {
|
|
307
|
+
const fresh = await probeHealth(backend);
|
|
308
|
+
healthCache.set(backend.id, { healthy: fresh, probed_at: Date.now() });
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
refreshInFlight.delete(backend.id);
|
|
312
|
+
}
|
|
313
|
+
})();
|
|
314
|
+
}
|
|
315
|
+
return cached.healthy;
|
|
316
|
+
}
|
|
317
|
+
// Cold — probe inline with timeout
|
|
318
|
+
try {
|
|
319
|
+
const fresh = await probeHealth(backend);
|
|
320
|
+
healthCache.set(backend.id, { healthy: fresh, probed_at: now });
|
|
321
|
+
return fresh;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// Treat unexpected probe failure as "unknown" — allow re-probe next call
|
|
325
|
+
healthCache.set(backend.id, { healthy: null, probed_at: now });
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/** Test-only helper: pre-seed the health cache. */
|
|
330
|
+
export function _seedHealth(backend_id, healthy) {
|
|
331
|
+
healthCache.set(backend_id, { healthy, probed_at: Date.now() });
|
|
332
|
+
}
|
|
333
|
+
// ─────────────────────────────────────────────────────────────────
|
|
334
|
+
// Round-robin / weighted selection
|
|
335
|
+
const rrCounters = new Map();
|
|
336
|
+
function roundRobin(key, candidates) {
|
|
337
|
+
if (candidates.length === 0) {
|
|
338
|
+
throw new Error("roundRobin called with empty candidates");
|
|
339
|
+
}
|
|
340
|
+
// Weighted? Expand into a virtual list; otherwise plain RR.
|
|
341
|
+
const totalWeight = candidates.reduce((s, b) => s + (b.weight ?? 1), 0);
|
|
342
|
+
if (candidates.some((b) => b.weight !== undefined)) {
|
|
343
|
+
const i = (rrCounters.get(key) ?? 0) % totalWeight;
|
|
344
|
+
rrCounters.set(key, i + 1);
|
|
345
|
+
let cum = 0;
|
|
346
|
+
for (const b of candidates) {
|
|
347
|
+
cum += b.weight ?? 1;
|
|
348
|
+
if (i < cum)
|
|
349
|
+
return b;
|
|
350
|
+
}
|
|
351
|
+
return candidates[candidates.length - 1];
|
|
352
|
+
}
|
|
353
|
+
const i = (rrCounters.get(key) ?? 0) % candidates.length;
|
|
354
|
+
rrCounters.set(key, i + 1);
|
|
355
|
+
return candidates[i];
|
|
356
|
+
}
|
|
357
|
+
// ─────────────────────────────────────────────────────────────────
|
|
358
|
+
// Routing
|
|
359
|
+
/**
|
|
360
|
+
* Apply the 6-step routing algorithm. Returns a Backend or null if no
|
|
361
|
+
* candidate is available (caller surfaces this as state: "error").
|
|
362
|
+
*
|
|
363
|
+
* `healthy_lookup` is injectable for tests; production passes
|
|
364
|
+
* `getCachedHealth`. The function is async because health may need
|
|
365
|
+
* a sync probe on first call.
|
|
366
|
+
*/
|
|
367
|
+
export async function chooseBackend(pool, opts, prompt, healthy_lookup = getCachedHealth) {
|
|
368
|
+
if (pool.length === 0)
|
|
369
|
+
return null;
|
|
370
|
+
// 1. Explicit pin — caller knows best, bypass all filters
|
|
371
|
+
if (opts.backend) {
|
|
372
|
+
const pinned = pool.find((b) => b.id === opts.backend);
|
|
373
|
+
return pinned ?? null;
|
|
374
|
+
}
|
|
375
|
+
// 1b. Chat-compatibility filter — qwen_spawn / qwen_oneshot go
|
|
376
|
+
// through /v1/chat/completions, which embedding/rerank backends
|
|
377
|
+
// do not implement. Unset modality is treated as 'text'; both
|
|
378
|
+
// 'text' and 'multimodal' are accepted (multimodal models can
|
|
379
|
+
// serve text-only chat). See bead qwen-coprocessor-stack-w63.
|
|
380
|
+
const chatPool = pool.filter((b) => {
|
|
381
|
+
const m = b.modality ?? "text";
|
|
382
|
+
// vision_only multimodal backends are dedicated to qwen_oneshot_vision
|
|
383
|
+
// and excluded from text chat (so a vision model doesn't absorb coding
|
|
384
|
+
// traffic meant for the text pool). See Backend.vision_only.
|
|
385
|
+
if (m === "multimodal" && b.vision_only === true)
|
|
386
|
+
return false;
|
|
387
|
+
return m === "text" || m === "multimodal";
|
|
388
|
+
});
|
|
389
|
+
if (chatPool.length === 0)
|
|
390
|
+
return null;
|
|
391
|
+
// 2. Tier filter
|
|
392
|
+
let candidates = opts.tier ? chatPool.filter((b) => b.tier === opts.tier) : [...chatPool];
|
|
393
|
+
if (candidates.length === 0)
|
|
394
|
+
candidates = [...chatPool]; // tier mismatch: fall back
|
|
395
|
+
// 3. Capacity classification + filter
|
|
396
|
+
const capacity = opts.capacity ?? classifyCapacity(prompt);
|
|
397
|
+
const capFiltered = candidates.filter((b) => b.capacity === capacity);
|
|
398
|
+
// If no backend has the desired capacity, allow any — better to serve
|
|
399
|
+
// sub-optimally than to fail.
|
|
400
|
+
if (capFiltered.length > 0)
|
|
401
|
+
candidates = capFiltered;
|
|
402
|
+
// 4. Health filter
|
|
403
|
+
const healthChecks = await Promise.all(candidates.map(async (b) => ({ b, healthy: await healthy_lookup(b) })));
|
|
404
|
+
// Treat null (unprobed/timeout) as healthy — optimistic; first real
|
|
405
|
+
// call will mark it false if the backend's actually down.
|
|
406
|
+
const live = healthChecks.filter((h) => h.healthy !== false).map((h) => h.b);
|
|
407
|
+
if (live.length > 0) {
|
|
408
|
+
// 5. Round-robin / weighted
|
|
409
|
+
return roundRobin(`${opts.tier ?? "any"}:${capacity}`, live);
|
|
410
|
+
}
|
|
411
|
+
// 6. No survivors after health: fall back to local (chat-compatible only)
|
|
412
|
+
const local = chatPool.filter((b) => b.tier === "local");
|
|
413
|
+
const localHealthy = await Promise.all(local.map(async (b) => ({ b, healthy: await healthy_lookup(b) })));
|
|
414
|
+
const localLive = localHealthy.filter((h) => h.healthy !== false).map((h) => h.b);
|
|
415
|
+
if (localLive.length > 0)
|
|
416
|
+
return roundRobin("fallback:local", localLive);
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Select a backend by declared modality. Used by `qwen_embed`,
|
|
421
|
+
* `qwen_rerank`, and `qwen_tokenize` — none of which go through the
|
|
422
|
+
* SDK / chat-completions path, so tier+capacity routing doesn't apply.
|
|
423
|
+
*
|
|
424
|
+
* - If `pinned_id` is supplied, return that backend iff it exists; the
|
|
425
|
+
* caller validates the modality match and surfaces `wrong_modality`.
|
|
426
|
+
* - Otherwise filter by `wanted` (treating unset modality as `'text'`),
|
|
427
|
+
* then round-robin across healthy candidates. `null` → no match.
|
|
428
|
+
*/
|
|
429
|
+
export async function chooseBackendByModality(pool, wanted, pinned_id, healthy_lookup = getCachedHealth) {
|
|
430
|
+
if (pool.length === 0)
|
|
431
|
+
return null;
|
|
432
|
+
if (pinned_id !== undefined) {
|
|
433
|
+
return pool.find((b) => b.id === pinned_id) ?? null;
|
|
434
|
+
}
|
|
435
|
+
const candidates = pool.filter((b) => (b.modality ?? "text") === wanted);
|
|
436
|
+
if (candidates.length === 0)
|
|
437
|
+
return null;
|
|
438
|
+
const healthChecks = await Promise.all(candidates.map(async (b) => ({ b, healthy: await healthy_lookup(b) })));
|
|
439
|
+
const live = healthChecks.filter((h) => h.healthy !== false).map((h) => h.b);
|
|
440
|
+
if (live.length === 0)
|
|
441
|
+
return null;
|
|
442
|
+
return roundRobin(`modality:${wanted}`, live);
|
|
443
|
+
}
|
|
444
|
+
//# sourceMappingURL=backends.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backends.js","sourceRoot":"","sources":["../src/backends.ts"],"names":[],"mappings":"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"}
|