@anjieyang/uncommon-route 0.1.0

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.
@@ -0,0 +1,46 @@
1
+ {
2
+ "id": "uncommon-route",
3
+ "name": "UncommonRoute",
4
+ "description": "SOTA smart LLM router — 98% accuracy, <1ms local routing, session persistence, spend control",
5
+ "configSchema": {
6
+ "type": "object",
7
+ "properties": {
8
+ "port": {
9
+ "type": "number",
10
+ "description": "Proxy port for the Python routing server (default: 8403)"
11
+ },
12
+ "upstream": {
13
+ "type": "string",
14
+ "description": "Upstream OpenAI-compatible API base URL"
15
+ },
16
+ "pythonPath": {
17
+ "type": "string",
18
+ "description": "Path to Python executable (default: python3)"
19
+ },
20
+ "spendLimits": {
21
+ "type": "object",
22
+ "description": "Spending limits: { perRequest, hourly, daily, session }",
23
+ "properties": {
24
+ "perRequest": { "type": "number" },
25
+ "hourly": { "type": "number" },
26
+ "daily": { "type": "number" },
27
+ "session": { "type": "number" }
28
+ }
29
+ }
30
+ }
31
+ },
32
+ "uiHints": {
33
+ "port": {
34
+ "label": "Proxy Port",
35
+ "placeholder": "8403"
36
+ },
37
+ "upstream": {
38
+ "label": "Upstream API URL",
39
+ "placeholder": "https://api.commonstack.ai/v1"
40
+ },
41
+ "pythonPath": {
42
+ "label": "Python Path",
43
+ "placeholder": "python3"
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://openclaw.ai/schemas/plugin-security.json",
3
+ "version": "1.0",
4
+ "plugin": "@anjieyang/uncommon-route",
5
+ "permissions": {
6
+ "network": {
7
+ "outbound": ["localhost:8403"],
8
+ "reason": "Communicates with the local UncommonRoute proxy server"
9
+ },
10
+ "subprocess": {
11
+ "allowed": true,
12
+ "commands": ["python3", "uncommon-route"],
13
+ "reason": "Spawns the Python routing proxy as a managed subprocess"
14
+ },
15
+ "filesystem": {
16
+ "read": ["~/.uncommon-route/"],
17
+ "write": ["~/.uncommon-route/spending.json"],
18
+ "reason": "Persistent spend control storage"
19
+ }
20
+ },
21
+ "dataFlow": {
22
+ "promptsLeaveProcess": false,
23
+ "description": "Routing decisions are made locally (<1ms). Prompts are only forwarded to the configured upstream API after routing."
24
+ },
25
+ "securityNotes": [
26
+ "All routing logic runs locally — no external API calls for classification",
27
+ "Spend control data stored at ~/.uncommon-route/spending.json with 0600 permissions",
28
+ "The Python subprocess binds only to 127.0.0.1 (localhost)"
29
+ ]
30
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@anjieyang/uncommon-route",
3
+ "version": "0.1.0",
4
+ "description": "OpenClaw plugin — UncommonRoute smart LLM router, 98% accuracy, <1ms local routing",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "openclaw": {
8
+ "extensions": [
9
+ "./src/index.js"
10
+ ]
11
+ },
12
+ "files": [
13
+ "src/",
14
+ "openclaw.plugin.json",
15
+ "openclaw.security.json"
16
+ ],
17
+ "keywords": [
18
+ "openclaw",
19
+ "openclaw-plugin",
20
+ "llm-router",
21
+ "smart-routing"
22
+ ],
23
+ "license": "MIT",
24
+ "peerDependencies": {
25
+ "openclaw": ">=2025.1.0"
26
+ },
27
+ "peerDependenciesMeta": {
28
+ "openclaw": {
29
+ "optional": true
30
+ }
31
+ }
32
+ }
package/src/index.js ADDED
@@ -0,0 +1,387 @@
1
+ /**
2
+ * OpenClaw Plugin — UncommonRoute
3
+ *
4
+ * Bridges the Python UncommonRoute router into OpenClaw's plugin system.
5
+ * Auto-installs Python package on first run (zero manual setup).
6
+ *
7
+ * Architecture:
8
+ * openclaw plugins install @anjieyang/uncommon-route
9
+ * → this plugin loads
10
+ * → ensures `uncommon-route` Python package is installed (pipx/uv/pip)
11
+ * → spawns `uncommon-route serve` as a managed subprocess
12
+ * → registerProvider pointing at localhost proxy
13
+ * → registerCommand for /route, /spend, /sessions
14
+ */
15
+
16
+ import { spawn, execSync } from "node:child_process";
17
+ import { setTimeout as sleep } from "node:timers/promises";
18
+
19
+ const VERSION = "0.1.0";
20
+ const DEFAULT_PORT = 8403;
21
+ const DEFAULT_UPSTREAM = "https://api.commonstack.ai/v1";
22
+ const HEALTH_TIMEOUT_MS = 15_000;
23
+ const HEALTH_POLL_MS = 500;
24
+ const PY_PACKAGE = "uncommon-route";
25
+
26
+ const MODELS = [
27
+ { id: "uncommon-route/auto", name: "UncommonRoute Auto", reasoning: false, input: 0, output: 0, ctx: 200_000, max: 16_384 },
28
+ { id: "moonshot/kimi-k2.5", name: "Kimi K2.5", reasoning: false, input: 0.60, output: 3.00, ctx: 128_000, max: 8_192 },
29
+ { id: "google/gemini-3.1-pro", name: "Gemini 3.1 Pro", reasoning: false, input: 2.00, output: 12.00, ctx: 200_000, max: 16_384 },
30
+ { id: "xai/grok-4-1-fast-reasoning", name: "Grok 4.1 Fast", reasoning: true, input: 0.20, output: 0.50, ctx: 200_000, max: 16_384 },
31
+ { id: "deepseek/deepseek-chat", name: "DeepSeek Chat", reasoning: false, input: 0.28, output: 0.42, ctx: 128_000, max: 8_192 },
32
+ { id: "deepseek/deepseek-reasoner", name: "DeepSeek Reasoner", reasoning: true, input: 0.28, output: 0.42, ctx: 128_000, max: 8_192 },
33
+ { id: "google/gemini-2.5-flash", name: "Gemini 2.5 Flash", reasoning: false, input: 0.30, output: 2.50, ctx: 200_000, max: 16_384 },
34
+ { id: "google/gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite", reasoning: false, input: 0.10, output: 0.40, ctx: 200_000, max: 16_384 },
35
+ { id: "openai/gpt-5.2", name: "GPT-5.2", reasoning: false, input: 1.75, output: 14.00, ctx: 200_000, max: 16_384 },
36
+ { id: "openai/o4-mini", name: "o4 Mini", reasoning: true, input: 1.10, output: 4.40, ctx: 200_000, max: 16_384 },
37
+ { id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", reasoning: false, input: 3.00, output: 15.00, ctx: 200_000, max: 16_384 },
38
+ ];
39
+
40
+ // ── Python dependency management ─────────────────────────────────────
41
+
42
+ function which(cmd) {
43
+ try {
44
+ return execSync(`which ${cmd}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function isPythonPackageInstalled(pythonPath) {
51
+ try {
52
+ execSync(`${pythonPath} -c "import uncommon_route"`, { stdio: "pipe" });
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Ensure the Python `uncommon-route` package is installed.
61
+ * Tries: pipx → uv → pip (with --user fallback).
62
+ * Returns the python executable to use.
63
+ */
64
+ function ensurePythonDeps(logger) {
65
+ const pythonCandidates = ["python3", "python"];
66
+ let pythonPath = null;
67
+
68
+ for (const candidate of pythonCandidates) {
69
+ const path = which(candidate);
70
+ if (path) {
71
+ try {
72
+ const ver = execSync(`${path} --version`, { encoding: "utf-8" }).trim();
73
+ const match = ver.match(/(\d+)\.(\d+)/);
74
+ if (match && (parseInt(match[1]) > 3 || (parseInt(match[1]) === 3 && parseInt(match[2]) >= 11))) {
75
+ pythonPath = path;
76
+ break;
77
+ }
78
+ } catch { /* skip */ }
79
+ }
80
+ }
81
+
82
+ if (!pythonPath) {
83
+ logger.error("Python 3.11+ not found. Install Python first: https://python.org");
84
+ logger.error(" macOS: brew install python@3.12");
85
+ logger.error(" Ubuntu: sudo apt install python3.12");
86
+ return null;
87
+ }
88
+
89
+ if (isPythonPackageInstalled(pythonPath)) {
90
+ logger.info(`Python package '${PY_PACKAGE}' already installed`);
91
+ return pythonPath;
92
+ }
93
+
94
+ logger.info(`Installing Python package '${PY_PACKAGE}'...`);
95
+
96
+ // Strategy 1: pipx (isolated, clean)
97
+ if (which("pipx")) {
98
+ try {
99
+ execSync(`pipx install ${PY_PACKAGE}`, { stdio: "pipe" });
100
+ logger.info(`Installed via pipx`);
101
+ return pythonPath;
102
+ } catch { /* fallthrough */ }
103
+ }
104
+
105
+ // Strategy 2: uv (fast, modern)
106
+ if (which("uv")) {
107
+ try {
108
+ execSync(`uv pip install ${PY_PACKAGE}`, { stdio: "pipe" });
109
+ logger.info(`Installed via uv`);
110
+ return pythonPath;
111
+ } catch { /* fallthrough */ }
112
+ }
113
+
114
+ // Strategy 3: pip install --user
115
+ try {
116
+ execSync(`${pythonPath} -m pip install ${PY_PACKAGE} --user --quiet`, { stdio: "pipe" });
117
+ logger.info(`Installed via pip --user`);
118
+ return pythonPath;
119
+ } catch { /* fallthrough */ }
120
+
121
+ // Strategy 4: pip install --break-system-packages (last resort on managed envs)
122
+ try {
123
+ execSync(`${pythonPath} -m pip install ${PY_PACKAGE} --user --break-system-packages --quiet`, { stdio: "pipe" });
124
+ logger.info(`Installed via pip (break-system-packages)`);
125
+ return pythonPath;
126
+ } catch (err) {
127
+ logger.error(`Failed to install '${PY_PACKAGE}': ${err.message}`);
128
+ logger.error(` Manual install: pip install ${PY_PACKAGE}`);
129
+ return null;
130
+ }
131
+ }
132
+
133
+ // ── Helpers ──────────────────────────────────────────────────────────
134
+
135
+ function buildModels(baseUrl) {
136
+ return {
137
+ baseUrl,
138
+ api: "openai-completions",
139
+ apiKey: "uncommon-route-local-proxy",
140
+ models: MODELS.map((m) => ({
141
+ id: m.id,
142
+ name: m.name,
143
+ api: "openai-completions",
144
+ reasoning: m.reasoning,
145
+ input: ["text"],
146
+ cost: { input: m.input, output: m.output, cacheRead: 0, cacheWrite: 0 },
147
+ contextWindow: m.ctx,
148
+ maxTokens: m.max,
149
+ })),
150
+ };
151
+ }
152
+
153
+ async function waitForHealth(port, timeoutMs = HEALTH_TIMEOUT_MS) {
154
+ const url = `http://127.0.0.1:${port}/health`;
155
+ const deadline = Date.now() + timeoutMs;
156
+ while (Date.now() < deadline) {
157
+ try {
158
+ const resp = await fetch(url, { signal: AbortSignal.timeout(2000) });
159
+ if (resp.ok) return true;
160
+ } catch { /* not ready */ }
161
+ await sleep(HEALTH_POLL_MS);
162
+ }
163
+ return false;
164
+ }
165
+
166
+ async function fetchJson(url) {
167
+ try {
168
+ const resp = await fetch(url, { signal: AbortSignal.timeout(5000) });
169
+ return resp.ok ? await resp.json() : null;
170
+ } catch { return null; }
171
+ }
172
+
173
+ async function postJson(url, body) {
174
+ try {
175
+ const resp = await fetch(url, {
176
+ method: "POST",
177
+ headers: { "content-type": "application/json" },
178
+ body: JSON.stringify(body),
179
+ signal: AbortSignal.timeout(5000),
180
+ });
181
+ return resp.ok ? await resp.json() : null;
182
+ } catch { return null; }
183
+ }
184
+
185
+ // ── Plugin ───────────────────────────────────────────────────────────
186
+
187
+ /** @type {import("node:child_process").ChildProcess | null} */
188
+ let pyProc = null;
189
+
190
+ const plugin = {
191
+ id: "uncommon-route",
192
+ name: "UncommonRoute",
193
+ description: "SOTA smart LLM router — 98% accuracy, <1ms local routing",
194
+ version: VERSION,
195
+
196
+ register(api) {
197
+ const isDisabled =
198
+ process.env.UNCOMMON_ROUTE_DISABLED === "true" ||
199
+ process.env.UNCOMMON_ROUTE_DISABLED === "1";
200
+ if (isDisabled) {
201
+ api.logger.info("UncommonRoute disabled via UNCOMMON_ROUTE_DISABLED");
202
+ return;
203
+ }
204
+
205
+ const cfg = api.pluginConfig || {};
206
+ const port = cfg.port || Number(process.env.UNCOMMON_ROUTE_PORT) || DEFAULT_PORT;
207
+ const upstream = cfg.upstream || process.env.UNCOMMON_ROUTE_UPSTREAM || DEFAULT_UPSTREAM;
208
+ const baseUrl = `http://127.0.0.1:${port}/v1`;
209
+
210
+ // 1. Register provider immediately (sync, models available right away)
211
+ api.registerProvider({
212
+ id: "uncommon-route",
213
+ label: "UncommonRoute",
214
+ docsPath: "https://github.com/anjieyang/UncommonRoute",
215
+ aliases: ["ur", "uncommon"],
216
+ envVars: [],
217
+ get models() { return buildModels(baseUrl); },
218
+ auth: [],
219
+ });
220
+
221
+ if (!api.config.models) api.config.models = { providers: {} };
222
+ if (!api.config.models.providers) api.config.models.providers = {};
223
+ api.config.models.providers["uncommon-route"] = buildModels(baseUrl);
224
+
225
+ api.logger.info(`UncommonRoute provider registered (${MODELS.length} models)`);
226
+
227
+ // 2. Register commands
228
+ api.registerCommand({
229
+ name: "route",
230
+ description: "Show which model UncommonRoute would pick for a prompt",
231
+ acceptsArgs: true,
232
+ requireAuth: false,
233
+ handler: async (ctx) => {
234
+ const prompt = (ctx.args || ctx.commandBody || "").trim();
235
+ if (!prompt) return { text: "Usage: /route <prompt>" };
236
+ try {
237
+ const resp = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
238
+ method: "POST",
239
+ headers: { "content-type": "application/json" },
240
+ body: JSON.stringify({ model: "uncommon-route/auto", messages: [{ role: "user", content: `/debug ${prompt}` }] }),
241
+ signal: AbortSignal.timeout(5000),
242
+ });
243
+ const data = await resp.json();
244
+ return { text: data?.choices?.[0]?.message?.content || "No response" };
245
+ } catch (err) {
246
+ return { text: `Error: ${err.message}. Is proxy running?`, isError: true };
247
+ }
248
+ },
249
+ });
250
+
251
+ api.registerCommand({
252
+ name: "spend",
253
+ description: "View or manage spending limits (/spend set hourly 5.00)",
254
+ acceptsArgs: true,
255
+ requireAuth: false,
256
+ handler: async (ctx) => {
257
+ const args = (ctx.args || "").trim();
258
+ const spendUrl = `http://127.0.0.1:${port}/v1/spend`;
259
+ if (!args || args === "status") {
260
+ const data = await fetchJson(spendUrl);
261
+ if (!data) return { text: "Proxy not running. Restart gateway.", isError: true };
262
+ const lines = ["**Spending Status**", ""];
263
+ const { limits, spent, remaining, calls } = data;
264
+ if (limits.per_request != null) lines.push(`Per-request: max $${limits.per_request.toFixed(2)}`);
265
+ if (limits.hourly != null) lines.push(`Hourly: $${spent.hourly.toFixed(4)} / $${limits.hourly.toFixed(2)} ($${remaining.hourly?.toFixed(4)} left)`);
266
+ if (limits.daily != null) lines.push(`Daily: $${spent.daily.toFixed(4)} / $${limits.daily.toFixed(2)} ($${remaining.daily?.toFixed(4)} left)`);
267
+ if (limits.session != null) lines.push(`Session: $${spent.session.toFixed(4)} / $${limits.session.toFixed(2)} ($${remaining.session?.toFixed(4)} left)`);
268
+ if (Object.keys(limits).length === 0) lines.push("No limits set. Use `/spend set hourly 5.00`");
269
+ lines.push("", `Total calls: ${calls}`);
270
+ return { text: lines.join("\n") };
271
+ }
272
+ const parts = args.split(/\s+/);
273
+ if (parts[0] === "set" && parts.length >= 3) {
274
+ const result = await postJson(spendUrl, { action: "set", window: parts[1], amount: parseFloat(parts[2]) });
275
+ return result ? { text: `Set ${parts[1]} limit: $${parseFloat(parts[2]).toFixed(2)}` } : { text: "Failed", isError: true };
276
+ }
277
+ if (parts[0] === "clear" && parts.length >= 2) {
278
+ const result = await postJson(spendUrl, { action: "clear", window: parts[1] });
279
+ return result ? { text: `Cleared ${parts[1]} limit` } : { text: "Failed", isError: true };
280
+ }
281
+ return { text: "Usage: `/spend [status | set <window> <amount> | clear <window>]`\nWindows: per_request, hourly, daily, session" };
282
+ },
283
+ });
284
+
285
+ api.registerCommand({
286
+ name: "sessions",
287
+ description: "View active routing sessions",
288
+ acceptsArgs: false,
289
+ requireAuth: false,
290
+ handler: async () => {
291
+ const data = await fetchJson(`http://127.0.0.1:${port}/v1/sessions`);
292
+ if (!data) return { text: "Proxy not running.", isError: true };
293
+ if (data.count === 0) return { text: "No active sessions" };
294
+ const lines = [`**Active Sessions** (${data.count})`, ""];
295
+ for (const s of data.sessions) {
296
+ lines.push(`• \`${s.id}\` model=${s.model} tier=${s.tier} requests=${s.requests} age=${s.age_s}s`);
297
+ }
298
+ return { text: lines.join("\n") };
299
+ },
300
+ });
301
+
302
+ // 3. Register service for lifecycle
303
+ api.registerService({
304
+ id: "uncommon-route-proxy",
305
+ start: () => {},
306
+ stop: async () => {
307
+ if (pyProc && !pyProc.killed) {
308
+ pyProc.kill("SIGTERM");
309
+ await sleep(1000);
310
+ if (!pyProc.killed) pyProc.kill("SIGKILL");
311
+ api.logger.info("UncommonRoute proxy stopped");
312
+ }
313
+ pyProc = null;
314
+ },
315
+ });
316
+
317
+ // 4. Apply spend limits from config
318
+ if (cfg.spendLimits) {
319
+ const applyLimits = async () => {
320
+ await sleep(3000);
321
+ for (const [window, amount] of Object.entries(cfg.spendLimits)) {
322
+ if (typeof amount === "number" && amount > 0) {
323
+ await postJson(`http://127.0.0.1:${port}/v1/spend`, { action: "set", window, amount });
324
+ }
325
+ }
326
+ api.logger.info("Spend limits applied from config");
327
+ };
328
+ applyLimits().catch(() => {});
329
+ }
330
+
331
+ // 5. Only spawn proxy in gateway mode
332
+ const isGateway = process.argv.some((a) => a === "gateway" || a === "start" || a === "serve");
333
+ if (!isGateway) {
334
+ api.logger.info("Not in gateway mode — proxy starts with `openclaw gateway start`");
335
+ return;
336
+ }
337
+
338
+ // 6. Auto-install Python deps + spawn proxy
339
+ const bootstrap = async () => {
340
+ const pythonPath = cfg.pythonPath || process.env.UNCOMMON_ROUTE_PYTHON || null;
341
+ let python = pythonPath;
342
+
343
+ if (!python || !isPythonPackageInstalled(python)) {
344
+ api.logger.info("Checking Python dependencies...");
345
+ python = ensurePythonDeps(api.logger);
346
+ if (!python) {
347
+ api.logger.error("Cannot start — Python setup failed. See errors above.");
348
+ return;
349
+ }
350
+ }
351
+
352
+ const args = ["-m", "uncommon_route.cli", "serve", "--port", String(port), "--upstream", upstream];
353
+ pyProc = spawn(python, args, {
354
+ stdio: ["ignore", "pipe", "pipe"],
355
+ env: { ...process.env, PYTHONUNBUFFERED: "1" },
356
+ });
357
+
358
+ pyProc.stdout?.on("data", (chunk) => {
359
+ const line = chunk.toString().trim();
360
+ if (line) api.logger.info(`[proxy] ${line}`);
361
+ });
362
+ pyProc.stderr?.on("data", (chunk) => {
363
+ const line = chunk.toString().trim();
364
+ if (line) api.logger.warn(`[proxy] ${line}`);
365
+ });
366
+ pyProc.on("exit", (code) => {
367
+ if (code !== null && code !== 0) api.logger.error(`Proxy exited with code ${code}`);
368
+ pyProc = null;
369
+ });
370
+
371
+ api.logger.info(`Starting proxy on port ${port}...`);
372
+ const healthy = await waitForHealth(port);
373
+ if (healthy) {
374
+ api.logger.info(`UncommonRoute ready at http://127.0.0.1:${port}`);
375
+ api.logger.info(`Default model: uncommon-route/auto`);
376
+ } else {
377
+ api.logger.warn("Proxy health check timed out — may need more time to start");
378
+ }
379
+ };
380
+
381
+ bootstrap().catch((err) => {
382
+ api.logger.error(`Bootstrap failed: ${err.message}`);
383
+ });
384
+ },
385
+ };
386
+
387
+ export default plugin;