@anjieyang/uncommon-route 0.2.9 → 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 +114 -19
- package/openclaw.plugin.json +2 -2
- package/openclaw.security.json +1 -1
- package/package.json +1 -1
- package/src/index.js +123 -63
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<p align="right"><strong>English</strong> | <a href="https://github.com/
|
|
1
|
+
<p align="right"><strong>English</strong> | <a href="https://github.com/CommonstackAI/UncommonRoute/blob/main/README.zh-CN.md">简体中文</a></p>
|
|
2
2
|
|
|
3
3
|
# @anjieyang/uncommon-route
|
|
4
4
|
|
|
5
|
-
OpenClaw plugin for [UncommonRoute](https://github.com/
|
|
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
|
|
|
@@ -17,7 +17,8 @@ This plugin:
|
|
|
17
17
|
- installs the Python `uncommon-route` package if needed
|
|
18
18
|
- starts `uncommon-route serve`
|
|
19
19
|
- registers the local provider with OpenClaw
|
|
20
|
-
- exposes the virtual routing
|
|
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
|
-
|
|
43
|
+
uncommon-route:
|
|
43
44
|
port: 8403
|
|
44
45
|
upstream: "https://api.commonstack.ai/v1"
|
|
45
46
|
spendLimits:
|
|
@@ -47,10 +48,14 @@ 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 |
|
|
53
|
-
|
|
58
|
+
| --- | --- |
|
|
54
59
|
| [Parallax](https://github.com/GradientHQ/parallax) | `http://127.0.0.1:3001/v1` |
|
|
55
60
|
| [Commonstack](https://commonstack.ai) | `https://api.commonstack.ai/v1` |
|
|
56
61
|
| OpenAI | `https://api.openai.com/v1` |
|
|
@@ -62,23 +67,22 @@ Parallax is best treated as an experimental local upstream for now: its public d
|
|
|
62
67
|
|
|
63
68
|
## What You Get
|
|
64
69
|
|
|
70
|
+
- a local OpenClaw provider backed by `http://127.0.0.1:8403/v1`
|
|
65
71
|
- `uncommon-route/auto` for balanced smart routing
|
|
66
|
-
- `uncommon-route/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- `uncommon-route/agentic` for tool-heavy workflows
|
|
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.
|
|
70
75
|
|
|
71
|
-
The router also keeps a fallback chain,
|
|
76
|
+
The router also keeps a fallback chain, records local feedback, and exposes a local dashboard at `http://127.0.0.1:8403/dashboard/`.
|
|
72
77
|
|
|
73
78
|
## OpenClaw Commands
|
|
74
79
|
|
|
75
80
|
| Command | Description |
|
|
76
|
-
|
|
81
|
+
| --- | --- |
|
|
77
82
|
| `/route <prompt>` | Preview which model the router would pick |
|
|
78
83
|
| `/spend status` | Show current spending and limits |
|
|
79
84
|
| `/spend set hourly 5.00` | Set an hourly spend limit |
|
|
80
|
-
| `/feedback ok
|
|
81
|
-
| `/sessions` | Show active routing sessions |
|
|
85
|
+
| `/feedback <signal>` | Use `ok`, `weak`, `strong`, `status`, or `rollback` to rate the last routing decision or inspect feedback state |
|
|
82
86
|
|
|
83
87
|
## Troubleshooting
|
|
84
88
|
|
|
@@ -89,20 +93,111 @@ If the plugin is installed but responses are failing:
|
|
|
89
93
|
3. Open `http://127.0.0.1:8403/health`.
|
|
90
94
|
4. Open `http://127.0.0.1:8403/dashboard/`.
|
|
91
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
|
+
|
|
92
187
|
## Benchmarks
|
|
93
188
|
|
|
94
189
|
Current repo benchmarks:
|
|
95
190
|
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
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
|
|
99
194
|
|
|
100
195
|
## Links
|
|
101
196
|
|
|
102
|
-
- [GitHub](https://github.com/
|
|
197
|
+
- [GitHub](https://github.com/CommonstackAI/UncommonRoute)
|
|
103
198
|
- [PyPI](https://pypi.org/project/uncommon-route/)
|
|
104
|
-
- [Full README](https://github.com/
|
|
199
|
+
- [Full README](https://github.com/CommonstackAI/UncommonRoute#readme)
|
|
105
200
|
|
|
106
201
|
## License
|
|
107
202
|
|
|
108
|
-
MIT
|
|
203
|
+
Modified MIT
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "uncommon-route",
|
|
3
3
|
"name": "UncommonRoute",
|
|
4
|
-
"description": "Local LLM router that cuts premium-model spend with smart routing,
|
|
4
|
+
"description": "Local LLM router that cuts premium-model spend with smart routing, local feedback, and spend control",
|
|
5
5
|
"configSchema": {
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
package/openclaw.security.json
CHANGED
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -10,35 +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
|
-
* →
|
|
13
|
+
* → syncs the discovered upstream pool into OpenClaw after startup
|
|
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.
|
|
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
|
|
27
|
-
{ id: "uncommon-route/auto", name: "UncommonRoute Auto", reasoning: false
|
|
28
|
-
{ id: "uncommon-route/
|
|
29
|
-
{ id: "uncommon-route/
|
|
30
|
-
{ id: "uncommon-route/free", name: "UncommonRoute Free", reasoning: false, input: 0, output: 0, ctx: 200_000, max: 16_384 },
|
|
31
|
-
{ id: "uncommon-route/agentic", name: "UncommonRoute Agentic", reasoning: true, input: 0, output: 0, ctx: 200_000, max: 16_384 },
|
|
32
|
-
{ id: "moonshot/kimi-k2.5", name: "Kimi K2.5", reasoning: false, input: 0.60, output: 3.00, ctx: 128_000, max: 8_192 },
|
|
33
|
-
{ 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 },
|
|
34
|
-
{ 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 },
|
|
35
|
-
{ id: "deepseek/deepseek-chat", name: "DeepSeek Chat", reasoning: false, input: 0.28, output: 0.42, ctx: 128_000, max: 8_192 },
|
|
36
|
-
{ id: "deepseek/deepseek-reasoner", name: "DeepSeek Reasoner", reasoning: true, input: 0.28, output: 0.42, ctx: 128_000, max: 8_192 },
|
|
37
|
-
{ 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 },
|
|
38
|
-
{ 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 },
|
|
39
|
-
{ id: "openai/gpt-5.2", name: "GPT-5.2", reasoning: false, input: 1.75, output: 14.00, ctx: 200_000, max: 16_384 },
|
|
40
|
-
{ id: "openai/o4-mini", name: "o4 Mini", reasoning: true, input: 1.10, output: 4.40, ctx: 200_000, max: 16_384 },
|
|
41
|
-
{ 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 },
|
|
42
33
|
];
|
|
43
34
|
|
|
44
35
|
// ── Python dependency management ─────────────────────────────────────
|
|
@@ -136,21 +127,71 @@ function ensurePythonDeps(logger) {
|
|
|
136
127
|
|
|
137
128
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
138
129
|
|
|
139
|
-
function
|
|
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 = []) {
|
|
140
187
|
return {
|
|
141
188
|
baseUrl,
|
|
142
189
|
api: "openai-completions",
|
|
143
190
|
apiKey: "uncommon-route-local-proxy",
|
|
144
|
-
models:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
reasoning: m.reasoning,
|
|
149
|
-
input: ["text"],
|
|
150
|
-
cost: { input: m.input, output: m.output, cacheRead: 0, cacheWrite: 0 },
|
|
151
|
-
contextWindow: m.ctx,
|
|
152
|
-
maxTokens: m.max,
|
|
153
|
-
})),
|
|
191
|
+
models: [
|
|
192
|
+
...VIRTUAL_MODELS.map((model) => modelEntry(model)),
|
|
193
|
+
...discoveredModelEntries(discoveredPool),
|
|
194
|
+
],
|
|
154
195
|
};
|
|
155
196
|
}
|
|
156
197
|
|
|
@@ -192,7 +233,7 @@ async function postJson(url, body) {
|
|
|
192
233
|
let pyProc = null;
|
|
193
234
|
|
|
194
235
|
const plugin = {
|
|
195
|
-
id: "
|
|
236
|
+
id: "uncommon-route",
|
|
196
237
|
name: "UncommonRoute",
|
|
197
238
|
description: "Local LLM router plugin that cuts premium-model spend with smart routing",
|
|
198
239
|
version: VERSION,
|
|
@@ -210,6 +251,34 @@ const plugin = {
|
|
|
210
251
|
const port = cfg.port || Number(process.env.UNCOMMON_ROUTE_PORT) || DEFAULT_PORT;
|
|
211
252
|
const upstream = cfg.upstream || process.env.UNCOMMON_ROUTE_UPSTREAM || DEFAULT_UPSTREAM;
|
|
212
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
|
+
}
|
|
213
282
|
|
|
214
283
|
if (!upstream) {
|
|
215
284
|
api.logger.warn("UncommonRoute: No upstream configured. Set UNCOMMON_ROUTE_UPSTREAM or configure 'upstream' in plugin config.");
|
|
@@ -217,21 +286,17 @@ const plugin = {
|
|
|
217
286
|
}
|
|
218
287
|
|
|
219
288
|
// 1. Register provider immediately (sync, models available right away)
|
|
289
|
+
applyProviderCatalog();
|
|
220
290
|
api.registerProvider({
|
|
221
291
|
id: "uncommon-route",
|
|
222
292
|
label: "UncommonRoute",
|
|
223
|
-
docsPath: "https://github.com/
|
|
293
|
+
docsPath: "https://github.com/CommonstackAI/UncommonRoute",
|
|
224
294
|
aliases: ["ur", "uncommon"],
|
|
225
295
|
envVars: [],
|
|
226
|
-
get models() { return
|
|
296
|
+
get models() { return providerCatalog; },
|
|
227
297
|
auth: [],
|
|
228
298
|
});
|
|
229
|
-
|
|
230
|
-
if (!api.config.models) api.config.models = { providers: {} };
|
|
231
|
-
if (!api.config.models.providers) api.config.models.providers = {};
|
|
232
|
-
api.config.models.providers["uncommon-route"] = buildModels(baseUrl);
|
|
233
|
-
|
|
234
|
-
api.logger.info(`UncommonRoute provider registered (${MODELS.length} models)`);
|
|
299
|
+
api.logger.info(`UncommonRoute provider registered (${providerCatalog.models.length} virtual route models)`);
|
|
235
300
|
|
|
236
301
|
// 2. Register commands
|
|
237
302
|
api.registerCommand({
|
|
@@ -350,23 +415,6 @@ const plugin = {
|
|
|
350
415
|
},
|
|
351
416
|
});
|
|
352
417
|
|
|
353
|
-
api.registerCommand({
|
|
354
|
-
name: "sessions",
|
|
355
|
-
description: "View active routing sessions",
|
|
356
|
-
acceptsArgs: false,
|
|
357
|
-
requireAuth: false,
|
|
358
|
-
handler: async () => {
|
|
359
|
-
const data = await fetchJson(`http://127.0.0.1:${port}/v1/sessions`);
|
|
360
|
-
if (!data) return { text: "Proxy not running.", isError: true };
|
|
361
|
-
if (data.count === 0) return { text: "No active sessions" };
|
|
362
|
-
const lines = [`**Active Sessions** (${data.count})`, ""];
|
|
363
|
-
for (const s of data.sessions) {
|
|
364
|
-
lines.push(`• \`${s.id}\` model=${s.model} tier=${s.tier} requests=${s.requests} age=${s.age_s}s`);
|
|
365
|
-
}
|
|
366
|
-
return { text: lines.join("\n") };
|
|
367
|
-
},
|
|
368
|
-
});
|
|
369
|
-
|
|
370
418
|
// 3. Register service for lifecycle
|
|
371
419
|
api.registerService({
|
|
372
420
|
id: "uncommon-route-proxy",
|
|
@@ -405,23 +453,34 @@ const plugin = {
|
|
|
405
453
|
|
|
406
454
|
// 6. Auto-install Python deps + spawn proxy
|
|
407
455
|
const bootstrap = async () => {
|
|
408
|
-
|
|
409
|
-
let python = pythonPath;
|
|
456
|
+
let cliBin = which("uncommon-route");
|
|
457
|
+
let python = cfg.pythonPath || process.env.UNCOMMON_ROUTE_PYTHON || null;
|
|
410
458
|
|
|
411
|
-
if (!python || !isPythonPackageInstalled(python)) {
|
|
459
|
+
if (!cliBin && (!python || !isPythonPackageInstalled(python))) {
|
|
412
460
|
api.logger.info("Checking Python dependencies...");
|
|
413
461
|
python = ensurePythonDeps(api.logger);
|
|
414
462
|
if (!python) {
|
|
415
463
|
api.logger.error("Cannot start — Python setup failed. See errors above.");
|
|
416
464
|
return;
|
|
417
465
|
}
|
|
466
|
+
cliBin = which("uncommon-route");
|
|
418
467
|
}
|
|
419
468
|
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
+
}
|
|
425
484
|
|
|
426
485
|
pyProc.stdout?.on("data", (chunk) => {
|
|
427
486
|
const line = chunk.toString().trim();
|
|
@@ -439,6 +498,7 @@ const plugin = {
|
|
|
439
498
|
api.logger.info(`Starting proxy on port ${port}...`);
|
|
440
499
|
const healthy = await waitForHealth(port);
|
|
441
500
|
if (healthy) {
|
|
501
|
+
await syncDiscoveredPool();
|
|
442
502
|
api.logger.info(`UncommonRoute ready at http://127.0.0.1:${port}`);
|
|
443
503
|
api.logger.info(`Default model: uncommon-route/auto`);
|
|
444
504
|
} else {
|