@anjieyang/uncommon-route 0.3.0 → 0.3.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @anjieyang/uncommon-route
4
4
 
5
- OpenClaw plugin for [UncommonRoute](https://github.com/CommonstackAI/UncommonRoute), the local LLM router that sends easy requests to cheaper models and saves stronger models for harder work.
5
+ OpenClaw plugin for [UncommonRoute](https://github.com/CommonstackAI/UncommonRoute), the local LLM router that classifies prompts, scores the discovered upstream pool, and routes virtual model IDs before forwarding requests upstream.
6
6
 
7
7
  If you use OpenClaw and want one local endpoint with smart routing behind it, this plugin is the shortest path.
8
8
 
@@ -18,6 +18,7 @@ This plugin:
18
18
  - starts `uncommon-route serve`
19
19
  - registers the local provider with OpenClaw
20
20
  - exposes the virtual routing modes like `uncommon-route/auto`
21
+ - syncs the discovered upstream pool into OpenClaw after the local proxy becomes healthy
21
22
 
22
23
  ## Install
23
24
 
@@ -39,7 +40,7 @@ Example plugin config:
39
40
  ```yaml
40
41
  plugins:
41
42
  entries:
42
- "@anjieyang/uncommon-route":
43
+ uncommon-route:
43
44
  port: 8403
44
45
  upstream: "https://api.commonstack.ai/v1"
45
46
  spendLimits:
@@ -47,6 +48,10 @@ plugins:
47
48
  daily: 20.00
48
49
  ```
49
50
 
51
+ > **Note:** OpenClaw uses the unscoped directory name `uncommon-route` as the
52
+ > entries key, not the full npm package name `@anjieyang/uncommon-route`.
53
+ > Config placed under the scoped name will not reach the plugin.
54
+
50
55
  Common upstream choices:
51
56
 
52
57
  | Provider | URL |
@@ -64,7 +69,9 @@ Parallax is best treated as an experimental local upstream for now: its public d
64
69
 
65
70
  - a local OpenClaw provider backed by `http://127.0.0.1:8403/v1`
66
71
  - `uncommon-route/auto` for balanced smart routing
67
- - hardcoded additional virtual modes: `uncommon-route/fast` and `uncommon-route/best`
72
+ - always-available virtual modes: `uncommon-route/fast` and `uncommon-route/best`
73
+
74
+ Once the proxy is up and `/v1/models/mapping` is available, the plugin refreshes the OpenClaw provider catalog from the discovered pool. If discovery is unavailable, the virtual modes still work and explicit passthrough model IDs can still be typed manually.
68
75
 
69
76
  The router also keeps a fallback chain, records local feedback, and exposes a local dashboard at `http://127.0.0.1:8403/dashboard/`.
70
77
 
@@ -86,13 +93,104 @@ If the plugin is installed but responses are failing:
86
93
  3. Open `http://127.0.0.1:8403/health`.
87
94
  4. Open `http://127.0.0.1:8403/dashboard/`.
88
95
 
96
+ ## Turn It Off Or Remove It
97
+
98
+ If you want to stop using the OpenClaw plugin, there are three different levels:
99
+
100
+ 1. stop routing traffic from OpenClaw
101
+ 2. clear all local UncommonRoute records and state
102
+ 3. fully uninstall the plugin and the Python package
103
+
104
+ ### 1. Stop routing traffic from OpenClaw
105
+
106
+ ```bash
107
+ openclaw plugins uninstall @anjieyang/uncommon-route
108
+ openclaw gateway restart
109
+ ```
110
+
111
+ If you also started `uncommon-route serve` manually, stop that too:
112
+
113
+ ```bash
114
+ uncommon-route stop
115
+ # or stop the foreground process with Ctrl+C
116
+ ```
117
+
118
+ If you used the config-patch fallback instead of the plugin, remove that registration too:
119
+
120
+ ```bash
121
+ uncommon-route openclaw uninstall
122
+ ```
123
+
124
+ ### 2. Clear all local records
125
+
126
+ By default, UncommonRoute stores local state under:
127
+
128
+ ```text
129
+ ~/.uncommon-route
130
+ ```
131
+
132
+ If you set `UNCOMMON_ROUTE_DATA_DIR`, it uses that directory instead.
133
+
134
+ That local data directory can contain:
135
+
136
+ - route stats and spending history
137
+ - dashboard-saved primary connection and routing overrides
138
+ - BYOK provider keys
139
+ - online-learning weights and feedback buffers
140
+ - learned aliases, model-experience memory, logs, and local artifacts
141
+
142
+ To clear **all** local records, stop the proxy first and then move or delete the active data directory:
143
+
144
+ ```bash
145
+ # Show the active data directory
146
+ echo "${UNCOMMON_ROUTE_DATA_DIR:-$HOME/.uncommon-route}"
147
+
148
+ # Recommended: move it aside as a backup first
149
+ mv "${UNCOMMON_ROUTE_DATA_DIR:-$HOME/.uncommon-route}" \
150
+ "${UNCOMMON_ROUTE_DATA_DIR:-$HOME/.uncommon-route}.backup-$(date +%Y%m%d-%H%M%S)"
151
+
152
+ # Or permanently delete it if you are sure
153
+ # rm -rf "${UNCOMMON_ROUTE_DATA_DIR:-$HOME/.uncommon-route}"
154
+ ```
155
+
156
+ If you only want to clear routing analytics, `uncommon-route stats reset` resets stats and pending feedback. It does **not** remove the rest of the local state.
157
+
158
+ ### 3. Fully uninstall
159
+
160
+ First remove the OpenClaw plugin or config-patch registration:
161
+
162
+ ```bash
163
+ openclaw plugins uninstall @anjieyang/uncommon-route
164
+ uncommon-route openclaw uninstall
165
+ openclaw gateway restart
166
+ ```
167
+
168
+ If you set environment variables for UncommonRoute, clear them:
169
+
170
+ ```bash
171
+ unset UNCOMMON_ROUTE_UPSTREAM
172
+ unset UNCOMMON_ROUTE_API_KEY
173
+ unset OPENAI_BASE_URL
174
+ unset ANTHROPIC_BASE_URL
175
+ ```
176
+
177
+ Then remove the Python package with the same tool you used to install it:
178
+
179
+ ```bash
180
+ pipx uninstall uncommon-route
181
+ # or
182
+ python -m pip uninstall uncommon-route
183
+ # or
184
+ pip uninstall uncommon-route
185
+ ```
186
+
89
187
  ## Benchmarks
90
188
 
91
189
  Current repo benchmarks:
92
190
 
93
- - 92.3% held-out routing accuracy
94
- - ~0.5ms average routing latency
95
- - 67% lower simulated cost than always using Claude Opus in a coding session
191
+ - 97.4% held-out routing accuracy on the current in-repo benchmark set
192
+ - ECE improves from 2.1% to 1.7% after temperature scaling
193
+ - 68% lower simulated cost than always using Claude Opus in a 131-request coding session
96
194
 
97
195
  ## Links
98
196
 
@@ -102,4 +200,4 @@ Current repo benchmarks:
102
200
 
103
201
  ## License
104
202
 
105
- MIT
203
+ Modified MIT
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "@anjieyang/uncommon-route",
2
+ "id": "uncommon-route",
3
3
  "name": "UncommonRoute",
4
4
  "description": "Local LLM router that cuts premium-model spend with smart routing, local feedback, and spend control",
5
5
  "configSchema": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://openclaw.ai/schemas/plugin-security.json",
3
3
  "version": "1.0",
4
- "plugin": "@anjieyang/uncommon-route",
4
+ "plugin": "uncommon-route",
5
5
  "permissions": {
6
6
  "network": {
7
7
  "outbound": ["localhost:8403"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anjieyang/uncommon-route",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "OpenClaw plugin for UncommonRoute, the local LLM router that cuts premium-model spend",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -10,33 +10,26 @@
10
10
  * → ensures `uncommon-route` Python package is installed (pipx/uv/pip)
11
11
  * → spawns `uncommon-route serve` as a managed subprocess
12
12
  * → registerProvider pointing at localhost proxy
13
+ * → syncs the discovered upstream pool into OpenClaw after startup
13
14
  * → registerCommand for /route, /spend, /feedback
14
15
  */
15
16
 
16
17
  import { spawn, execSync } from "node:child_process";
17
18
  import { setTimeout as sleep } from "node:timers/promises";
18
19
 
19
- const VERSION = "0.3.0";
20
+ const VERSION = "0.3.1";
20
21
  const DEFAULT_PORT = 8403;
21
22
  const DEFAULT_UPSTREAM = "";
22
23
  const HEALTH_TIMEOUT_MS = 15_000;
23
24
  const HEALTH_POLL_MS = 500;
24
25
  const PY_PACKAGE = "uncommon-route";
26
+ const DEFAULT_CONTEXT_WINDOW = 200_000;
27
+ const DEFAULT_MAX_TOKENS = 16_384;
25
28
 
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: "uncommon-route/fast", name: "UncommonRoute Fast", reasoning: false, input: 0, output: 0, ctx: 200_000, max: 16_384 },
29
- { id: "uncommon-route/best", name: "UncommonRoute Best", reasoning: true, input: 0, output: 0, ctx: 200_000, max: 16_384 },
30
- { id: "moonshot/kimi-k2.5", name: "Kimi K2.5", reasoning: false, input: 0.60, output: 3.00, ctx: 128_000, max: 8_192 },
31
- { 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 },
32
- { 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 },
33
- { id: "deepseek/deepseek-chat", name: "DeepSeek Chat", reasoning: false, input: 0.28, output: 0.42, ctx: 128_000, max: 8_192 },
34
- { id: "deepseek/deepseek-reasoner", name: "DeepSeek Reasoner", reasoning: true, input: 0.28, output: 0.42, ctx: 128_000, max: 8_192 },
35
- { 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 },
36
- { 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 },
37
- { id: "openai/gpt-5.2", name: "GPT-5.2", reasoning: false, input: 1.75, output: 14.00, ctx: 200_000, max: 16_384 },
38
- { id: "openai/o4-mini", name: "o4 Mini", reasoning: true, input: 1.10, output: 4.40, ctx: 200_000, max: 16_384 },
39
- { 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 },
29
+ const VIRTUAL_MODELS = [
30
+ { id: "uncommon-route/auto", name: "UncommonRoute Auto", reasoning: false },
31
+ { id: "uncommon-route/fast", name: "UncommonRoute Fast", reasoning: false },
32
+ { id: "uncommon-route/best", name: "UncommonRoute Best", reasoning: true },
40
33
  ];
41
34
 
42
35
  // ── Python dependency management ─────────────────────────────────────
@@ -134,21 +127,71 @@ function ensurePythonDeps(logger) {
134
127
 
135
128
  // ── Helpers ──────────────────────────────────────────────────────────
136
129
 
137
- function buildModels(baseUrl) {
130
+ function toFiniteNumber(value) {
131
+ const num = Number(value);
132
+ return Number.isFinite(num) ? num : 0;
133
+ }
134
+
135
+ function modelEntry({
136
+ id,
137
+ name = id,
138
+ reasoning = false,
139
+ input = 0,
140
+ output = 0,
141
+ cacheRead = 0,
142
+ cacheWrite = 0,
143
+ ctx = DEFAULT_CONTEXT_WINDOW,
144
+ max = DEFAULT_MAX_TOKENS,
145
+ }) {
146
+ return {
147
+ id,
148
+ name,
149
+ api: "openai-completions",
150
+ reasoning,
151
+ input: ["text"],
152
+ cost: { input, output, cacheRead, cacheWrite },
153
+ contextWindow: ctx,
154
+ maxTokens: max,
155
+ };
156
+ }
157
+
158
+ function discoveredModelEntries(discoveredPool) {
159
+ if (!Array.isArray(discoveredPool)) return [];
160
+
161
+ const seen = new Set(VIRTUAL_MODELS.map((model) => model.id));
162
+ const models = [];
163
+
164
+ for (const row of discoveredPool) {
165
+ const id = typeof row?.id === "string" ? row.id.trim() : "";
166
+ if (!id || seen.has(id)) continue;
167
+
168
+ seen.add(id);
169
+ const pricing = row?.pricing ?? {};
170
+ const capabilities = row?.capabilities ?? {};
171
+
172
+ models.push(modelEntry({
173
+ id,
174
+ name: id,
175
+ reasoning: Boolean(capabilities.reasoning),
176
+ input: toFiniteNumber(pricing.input),
177
+ output: toFiniteNumber(pricing.output),
178
+ cacheRead: toFiniteNumber(pricing.cached_input),
179
+ cacheWrite: toFiniteNumber(pricing.cache_write),
180
+ }));
181
+ }
182
+
183
+ return models;
184
+ }
185
+
186
+ function buildModels(baseUrl, discoveredPool = []) {
138
187
  return {
139
188
  baseUrl,
140
189
  api: "openai-completions",
141
190
  apiKey: "uncommon-route-local-proxy",
142
- models: MODELS.map((m) => ({
143
- id: m.id,
144
- name: m.name,
145
- api: "openai-completions",
146
- reasoning: m.reasoning,
147
- input: ["text"],
148
- cost: { input: m.input, output: m.output, cacheRead: 0, cacheWrite: 0 },
149
- contextWindow: m.ctx,
150
- maxTokens: m.max,
151
- })),
191
+ models: [
192
+ ...VIRTUAL_MODELS.map((model) => modelEntry(model)),
193
+ ...discoveredModelEntries(discoveredPool),
194
+ ],
152
195
  };
153
196
  }
154
197
 
@@ -190,7 +233,7 @@ async function postJson(url, body) {
190
233
  let pyProc = null;
191
234
 
192
235
  const plugin = {
193
- id: "@anjieyang/uncommon-route",
236
+ id: "uncommon-route",
194
237
  name: "UncommonRoute",
195
238
  description: "Local LLM router plugin that cuts premium-model spend with smart routing",
196
239
  version: VERSION,
@@ -208,6 +251,34 @@ const plugin = {
208
251
  const port = cfg.port || Number(process.env.UNCOMMON_ROUTE_PORT) || DEFAULT_PORT;
209
252
  const upstream = cfg.upstream || process.env.UNCOMMON_ROUTE_UPSTREAM || DEFAULT_UPSTREAM;
210
253
  const baseUrl = `http://127.0.0.1:${port}/v1`;
254
+ let discoveredPool = [];
255
+ let providerCatalog = buildModels(baseUrl, discoveredPool);
256
+
257
+ function applyProviderCatalog(nextPool = discoveredPool) {
258
+ discoveredPool = Array.isArray(nextPool) ? nextPool : [];
259
+ providerCatalog = buildModels(baseUrl, discoveredPool);
260
+ if (!api.config.models) api.config.models = { providers: {} };
261
+ if (!api.config.models.providers) api.config.models.providers = {};
262
+ api.config.models.providers["uncommon-route"] = providerCatalog;
263
+ return providerCatalog;
264
+ }
265
+
266
+ async function syncDiscoveredPool() {
267
+ const mapping = await fetchJson(`http://127.0.0.1:${port}/v1/models/mapping`);
268
+ if (!mapping) {
269
+ api.logger.warn("Could not read /v1/models/mapping; keeping OpenClaw provider catalog on virtual routes only.");
270
+ return false;
271
+ }
272
+
273
+ const nextCatalog = applyProviderCatalog(mapping.pool);
274
+ const discoveredCount = Math.max(nextCatalog.models.length - VIRTUAL_MODELS.length, 0);
275
+ if (mapping.discovered && discoveredCount > 0) {
276
+ api.logger.info(`Synced ${discoveredCount} discovered upstream models into OpenClaw provider catalog`);
277
+ } else {
278
+ api.logger.info("Upstream discovery unavailable; OpenClaw provider catalog remains virtual-mode only");
279
+ }
280
+ return true;
281
+ }
211
282
 
212
283
  if (!upstream) {
213
284
  api.logger.warn("UncommonRoute: No upstream configured. Set UNCOMMON_ROUTE_UPSTREAM or configure 'upstream' in plugin config.");
@@ -215,21 +286,17 @@ const plugin = {
215
286
  }
216
287
 
217
288
  // 1. Register provider immediately (sync, models available right away)
289
+ applyProviderCatalog();
218
290
  api.registerProvider({
219
291
  id: "uncommon-route",
220
292
  label: "UncommonRoute",
221
293
  docsPath: "https://github.com/CommonstackAI/UncommonRoute",
222
294
  aliases: ["ur", "uncommon"],
223
295
  envVars: [],
224
- get models() { return buildModels(baseUrl); },
296
+ get models() { return providerCatalog; },
225
297
  auth: [],
226
298
  });
227
-
228
- if (!api.config.models) api.config.models = { providers: {} };
229
- if (!api.config.models.providers) api.config.models.providers = {};
230
- api.config.models.providers["uncommon-route"] = buildModels(baseUrl);
231
-
232
- api.logger.info(`UncommonRoute provider registered (${MODELS.length} models)`);
299
+ api.logger.info(`UncommonRoute provider registered (${providerCatalog.models.length} virtual route models)`);
233
300
 
234
301
  // 2. Register commands
235
302
  api.registerCommand({
@@ -386,23 +453,34 @@ const plugin = {
386
453
 
387
454
  // 6. Auto-install Python deps + spawn proxy
388
455
  const bootstrap = async () => {
389
- const pythonPath = cfg.pythonPath || process.env.UNCOMMON_ROUTE_PYTHON || null;
390
- let python = pythonPath;
456
+ let cliBin = which("uncommon-route");
457
+ let python = cfg.pythonPath || process.env.UNCOMMON_ROUTE_PYTHON || null;
391
458
 
392
- if (!python || !isPythonPackageInstalled(python)) {
459
+ if (!cliBin && (!python || !isPythonPackageInstalled(python))) {
393
460
  api.logger.info("Checking Python dependencies...");
394
461
  python = ensurePythonDeps(api.logger);
395
462
  if (!python) {
396
463
  api.logger.error("Cannot start — Python setup failed. See errors above.");
397
464
  return;
398
465
  }
466
+ cliBin = which("uncommon-route");
399
467
  }
400
468
 
401
- const args = ["-m", "uncommon_route.cli", "serve", "--port", String(port), "--upstream", upstream];
402
- pyProc = spawn(python, args, {
403
- stdio: ["ignore", "pipe", "pipe"],
404
- env: { ...process.env, PYTHONUNBUFFERED: "1" },
405
- });
469
+ const serveArgs = ["serve", "--port", String(port), "--upstream", upstream];
470
+ if (cliBin) {
471
+ pyProc = spawn(cliBin, serveArgs, {
472
+ stdio: ["ignore", "pipe", "pipe"],
473
+ env: { ...process.env, PYTHONUNBUFFERED: "1" },
474
+ });
475
+ } else if (python) {
476
+ pyProc = spawn(python, ["-m", "uncommon_route.cli", ...serveArgs], {
477
+ stdio: ["ignore", "pipe", "pipe"],
478
+ env: { ...process.env, PYTHONUNBUFFERED: "1" },
479
+ });
480
+ } else {
481
+ api.logger.error("Cannot start — neither uncommon-route CLI nor Python module found.");
482
+ return;
483
+ }
406
484
 
407
485
  pyProc.stdout?.on("data", (chunk) => {
408
486
  const line = chunk.toString().trim();
@@ -420,6 +498,7 @@ const plugin = {
420
498
  api.logger.info(`Starting proxy on port ${port}...`);
421
499
  const healthy = await waitForHealth(port);
422
500
  if (healthy) {
501
+ await syncDiscoveredPool();
423
502
  api.logger.info(`UncommonRoute ready at http://127.0.0.1:${port}`);
424
503
  api.logger.info(`Default model: uncommon-route/auto`);
425
504
  } else {