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 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
+ ```
@@ -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":"AAAA,+BAA+B;AAC/B,EAAE;AACF,4DAA4D;AAC5D,EAAE;AACF,uEAAuE;AACvE,sEAAsE;AACtE,sEAAsE;AACtE,yBAAyB;AACzB,EAAE;AACF,qEAAqE;AACrE,mEAAmE;AAEnE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAGxC,MAAM,GAAG,GAAG,YAAY,CAAC,eAAe,CAAC,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wFAAwF;AACxF,MAAM,kBAAkB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,yBAAyB,CAAC,CAAC;AAEtE,MAAM,UAAU,YAAY;IAC1B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAChD,OAAO,QAAQ,IAAI,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,YAAY,EAAE,EAAE,aAAa,CAAC,CAAC;AAC7C,CAAC;AAqBD,IAAI,YAAY,GAAuB,IAAI,CAAC;AAE5C,oEAAoE;AACpE,MAAM,UAAU,iBAAiB;IAC/B,YAAY,GAAG,IAAI,CAAC;AACtB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,aAAa,EAAE,CAAC;IAC7B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,YAAY,IAAI,YAAY,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;QACrD,OAAO,YAAY,CAAC,MAAM,CAAC;IAC7B,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAC;QAClD,YAAY,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QACnC,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,IAAI,CACN,EAAE,UAAU,EAAE,gBAAgB,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAC7F,sEAAsE,CACvE,CAAC;QACF,YAAY,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB;IACzB,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnF,OAAO,GAAG,CAAC,QAAQ,CAAC;AACtB,CAAC;AA+BD,MAAM,0BAA0B,GAAG,OAAO,CAAC;AAC3C,MAAM,sBAAsB,GAAG,CAAC,CAAC;AACjC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAE/B,SAAS,eAAe,CAAC,IAAY,EAAE,GAAsB;IAC3D,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IACtB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACxD,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC,IAAI,CACN,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,EAC/D,6CAA6C,CAC9C,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,MAAyB,OAAO,CAAC,GAAG,EACpC,OAAiB;IAEjB,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,MAAM,SAAS,GAAG,GAAG,EAAE,cAAc,CAAC;IAEtC,MAAM,SAAS,GAAG,eAAe,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;IAClE,MAAM,SAAS,GACb,OAAO,SAAS,EAAE,kBAAkB,KAAK,QAAQ,IAAI,SAAS,CAAC,kBAAkB,IAAI,CAAC;QACpF,CAAC,CAAC,SAAS,CAAC,kBAAkB;QAC9B,CAAC,CAAC,IAAI,CAAC;IACX,MAAM,iBAAiB,GACrB,OAAO,KAAK,SAAS,IAAI,OAAO,OAAO,CAAC,QAAQ,KAAK,QAAQ,IAAI,OAAO,CAAC,QAAQ,GAAG,CAAC;QACnF,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,GAAG,iBAAiB,CAAC;QAClD,CAAC,CAAC,IAAI,CAAC;IAEX,MAAM,WAAW,GAAG,eAAe,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;IAChE,MAAM,WAAW,GACf,OAAO,SAAS,EAAE,cAAc,KAAK,QAAQ,IAAI,SAAS,CAAC,cAAc,IAAI,CAAC;QAC5E,CAAC,CAAC,SAAS,CAAC,cAAc;QAC1B,CAAC,CAAC,IAAI,CAAC;IAEX,OAAO;QACL,kBAAkB,EAChB,SAAS,IAAI,SAAS,IAAI,iBAAiB,IAAI,0BAA0B;QAC3E,cAAc,EAAE,WAAW,IAAI,WAAW,IAAI,sBAAsB;KACrE,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B;IACzC,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,kBAAkB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1F,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,GAAG,CAAC,kBAAkB,CAAC;AAChC,CAAC;AAED,oEAAoE;AACpE,gBAAgB;AAEhB,MAAM,eAAe,GAAY;IAC/B,EAAE,EAAE,WAAW;IACf,GAAG,EAAE,0BAA0B;IAC/B,KAAK,EAAE,sBAAsB;IAC7B,IAAI,EAAE,OAAO;IACb,QAAQ,EAAE,MAAM;CACjB,CAAC;AAEF,MAAM,aAAa,GAAG,MAAM,CAAC;AAC7B,MAAM,qBAAqB,GAAG,KAAK,CAAC;AAEpC,MAAM,sBAAsB,GAAG,+BAA+B,CAAC;AAC/D,MAAM,uBAAuB,GAAG,KAAK,CAAC;AAEtC;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAA6B;IAC/D,MAAM,KAAK,GAAG,YAAY,EAAE,CAAC;IAC7B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,kBAAkB;IAClB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACzC,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC/C,OAAO,MAAmB,CAAC;YAC7B,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,IAAI,CACN,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,EAC/C,2EAA2E,CAC5E,CAAC;QACJ,CAAC;IACH,CAAC;IAED,iBAAiB;IACjB,MAAM,QAAQ,GAAG,kBAAkB,EAAE,CAAC;IACtC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,aAAa;IACb,OAAO,CAAC,eAAe,CAAC,CAAC;AAC3B,CAAC;AAED,oEAAoE;AACpE,0BAA0B;AAE1B;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,OAAO,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACnC,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,CAAC,CAAC;IAC7B,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC;IACvD,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,MAAM,SAAS,GAAG,QAAQ,CACxB,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,IAAI,MAAM,CAAC,uBAAuB,CAAC,EAC/E,EAAE,CACH,CAAC;IACF,IAAI,YAAY,CAAC,MAAM,CAAC,IAAI,SAAS;QAAE,OAAO,OAAO,CAAC;IAEtD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,IAAI,sBAAsB,CAAC;IAC7E,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACrF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IACnC,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACjF,IAAI,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,OAAO,CAAC;IACrC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAUD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAuB,CAAC;AACnD,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;AAE1C,kDAAkD;AAClD,MAAM,UAAU,gBAAgB;IAC9B,WAAW,CAAC,KAAK,EAAE,CAAC;IACpB,eAAe,CAAC,KAAK,EAAE,CAAC;IACxB,UAAU,CAAC,KAAK,EAAE,CAAC;AACrB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAgB;IAChD,MAAM,QAAQ,GAAG,KAAK,EAAE,GAAW,EAAE,SAAiB,EAAoB,EAAE;QAC1E,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;QAClD,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;YACjE,OAAO,CAAC,CAAC,EAAE,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAE9C,gDAAgD;IAChD,IAAI,MAAM,QAAQ,CAAC,GAAG,QAAQ,SAAS,EAAE,qBAAqB,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7E,uEAAuE;IACvE,yBAAyB;IACzB,IAAI,MAAM,QAAQ,CAAC,GAAG,OAAO,SAAS,EAAE,qBAAqB,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5E,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,kBAAkB,CAAC,CAAS;IACnC,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAgB;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAE3C,IAAI,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,GAAG,aAAa,EAAE,CAAC;QACrD,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC;IAED,IAAI,MAAM,EAAE,CAAC;QACX,sDAAsD;QACtD,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;YACrC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAChC,KAAK,CAAC,KAAK,IAAI,EAAE;gBACf,IAAI,CAAC;oBACH,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,CAAC;oBACzC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBACzE,CAAC;wBAAS,CAAC;oBACT,eAAe,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACrC,CAAC;YACH,CAAC,CAAC,EAAE,CAAC;QACP,CAAC;QACD,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC;IAED,mCAAmC;IACnC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,CAAC;QACzC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;QAChE,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;QACzE,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;QAC/D,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,WAAW,CAAC,UAAkB,EAAE,OAAuB;IACrE,WAAW,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AAClE,CAAC;AAED,oEAAoE;AACpE,mCAAmC;AAEnC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;AAE7C,SAAS,UAAU,CAAC,GAAW,EAAE,UAAqB;IACpD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,4DAA4D;IAC5D,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACxE,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,EAAE,CAAC;QACnD,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,WAAW,CAAC;QACnD,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3B,IAAI,GAAG,GAAG,CAAC,CAAC;QACZ,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YAC3B,GAAG,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;YACrB,IAAI,CAAC,GAAG,GAAG;gBAAE,OAAO,CAAC,CAAC;QACxB,CAAC;QACD,OAAO,UAAU,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;IAC5C,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC;IACzD,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,OAAO,UAAU,CAAC,CAAC,CAAE,CAAC;AACxB,CAAC;AAED,oEAAoE;AACpE,UAAU;AAEV;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAe,EACf,IAAe,EACf,MAAc,EACd,iBAA0D,eAAe;IAEzE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnC,0DAA0D;IAC1D,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,OAAO,CAAC,CAAC;QACvD,OAAO,MAAM,IAAI,IAAI,CAAC;IACxB,CAAC;IAED,+DAA+D;IAC/D,gEAAgE;IAChE,8DAA8D;IAC9D,8DAA8D;IAC9D,8DAA8D;IAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACjC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,MAAM,CAAC;QAC/B,uEAAuE;QACvE,uEAAuE;QACvE,6DAA6D;QAC7D,IAAI,CAAC,KAAK,YAAY,IAAI,CAAC,CAAC,WAAW,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC/D,OAAO,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,YAAY,CAAC;IAC5C,CAAC,CAAC,CAAC;IACH,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvC,iBAAiB;IACjB,IAAI,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC;IAC1F,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,UAAU,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,2BAA2B;IAEpF,sCAAsC;IACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC3D,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;IACtE,sEAAsE;IACtE,8BAA8B;IAC9B,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC;QAAE,UAAU,GAAG,WAAW,CAAC;IAErD,mBAAmB;IACnB,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,GAAG,CACpC,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CACvE,CAAC;IACF,oEAAoE;IACpE,0DAA0D;IAC1D,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7E,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,4BAA4B;QAC5B,OAAO,UAAU,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,KAAK,IAAI,QAAQ,EAAE,EAAE,IAAI,CAAC,CAAC;IAC/D,CAAC;IAED,0EAA0E;IAC1E,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;IACzD,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,GAAG,CACpC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAClE,CAAC;IACF,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClF,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,UAAU,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;IAEzE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,IAAe,EACf,MAAwC,EACxC,SAAkB,EAClB,iBAA0D,eAAe;IAEzE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS,CAAC,IAAI,IAAI,CAAC;IACtD,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,MAAM,CAAC,KAAK,MAAM,CAAC,CAAC;IACzE,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,GAAG,CACpC,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CACvE,CAAC;IACF,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnC,OAAO,UAAU,CAAC,YAAY,MAAM,EAAE,EAAE,IAAI,CAAC,CAAC;AAChD,CAAC"}