@blockrun/clawrouter 0.12.63 → 0.12.65
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 +55 -55
- package/dist/cli.js +50 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.js +57 -16
- package/dist/index.js.map +1 -1
- package/docs/anthropic-cost-savings.md +90 -85
- package/docs/architecture.md +12 -12
- package/docs/{blog-openclaw-cost-overruns.md → clawrouter-cuts-llm-api-costs-500x.md} +27 -27
- package/docs/clawrouter-vs-openrouter-llm-routing-comparison.md +280 -0
- package/docs/configuration.md +2 -2
- package/docs/image-generation.md +39 -39
- package/docs/{blog-benchmark-2026-03.md → llm-router-benchmark-46-models-sub-1ms-routing.md} +61 -64
- package/docs/routing-profiles.md +6 -6
- package/docs/{technical-routing-2026-03.md → smart-llm-router-14-dimension-classifier.md} +29 -28
- package/docs/worker-network.md +438 -347
- package/package.json +3 -2
- package/scripts/reinstall.sh +31 -6
- package/scripts/update.sh +6 -1
- package/docs/assets/blockrun-248-day-cost-overrun-problem.png +0 -0
- package/docs/assets/blockrun-clawrouter-7-layer-token-compression-openclaw.png +0 -0
- package/docs/assets/blockrun-clawrouter-observation-compression-97-percent-token-savings.png +0 -0
- package/docs/assets/blockrun-clawrouter-openclaw-agentic-proxy-architecture.png +0 -0
- package/docs/assets/blockrun-clawrouter-openclaw-automatic-tier-routing-model-selection.png +0 -0
- package/docs/assets/blockrun-clawrouter-openclaw-error-classification-retry-storm-prevention.png +0 -0
- package/docs/assets/blockrun-clawrouter-openclaw-session-memory-journaling-vs-context-compounding.png +0 -0
- package/docs/assets/blockrun-clawrouter-vs-openclaw-standalone-comparison-production-safety.png +0 -0
- package/docs/assets/blockrun-clawrouter-x402-usdc-micropayment-wallet-budget-control.png +0 -0
- package/docs/assets/blockrun-openclaw-inference-layer-blind-spots.png +0 -0
- package/docs/plans/2026-02-03-smart-routing-design.md +0 -267
- package/docs/plans/2026-02-13-e2e-docker-deployment.md +0 -1260
- package/docs/plans/2026-02-28-worker-network.md +0 -947
- package/docs/plans/2026-03-18-error-classification.md +0 -574
- package/docs/plans/2026-03-19-exclude-models.md +0 -538
- package/docs/vs-openrouter.md +0 -157
|
@@ -1,538 +0,0 @@
|
|
|
1
|
-
# Exclude Models Feature — Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
-
|
|
5
|
-
**Goal:** Let users exclude specific models from routing via `/exclude` Telegram command, persisted to disk.
|
|
6
|
-
|
|
7
|
-
**Architecture:** New `exclude-models.json` file at `~/.openclaw/blockrun/` stores the exclusion list. A `filterByExcludeList()` function in `selector.ts` filters the fallback chain (same safety pattern as existing filters). The `/exclude` command manages the list via add/remove/clear subcommands. The proxy loads the list at startup and re-reads on each request (hot-reload).
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** TypeScript, Node.js fs, existing ClawRouter command pattern
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
### Task 1: Exclude List Persistence Module
|
|
14
|
-
|
|
15
|
-
**Files:**
|
|
16
|
-
- Create: `src/exclude-models.ts`
|
|
17
|
-
- Test: `src/exclude-models.test.ts`
|
|
18
|
-
|
|
19
|
-
**Step 1: Write the failing test**
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
// src/exclude-models.test.ts
|
|
23
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
24
|
-
import { join } from "node:path";
|
|
25
|
-
import { mkdirSync, rmSync, existsSync } from "node:fs";
|
|
26
|
-
import { tmpdir } from "node:os";
|
|
27
|
-
import {
|
|
28
|
-
loadExcludeList,
|
|
29
|
-
addExclusion,
|
|
30
|
-
removeExclusion,
|
|
31
|
-
clearExclusions,
|
|
32
|
-
} from "./exclude-models.js";
|
|
33
|
-
|
|
34
|
-
const TEST_DIR = join(tmpdir(), "clawrouter-test-exclude-" + Date.now());
|
|
35
|
-
const TEST_FILE = join(TEST_DIR, "exclude-models.json");
|
|
36
|
-
|
|
37
|
-
describe("exclude-models", () => {
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
mkdirSync(TEST_DIR, { recursive: true });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("returns empty set when file does not exist", () => {
|
|
47
|
-
const list = loadExcludeList(TEST_FILE);
|
|
48
|
-
expect(list.size).toBe(0);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("adds a model and persists to disk", () => {
|
|
52
|
-
addExclusion("nvidia/gpt-oss-120b", TEST_FILE);
|
|
53
|
-
const list = loadExcludeList(TEST_FILE);
|
|
54
|
-
expect(list.has("nvidia/gpt-oss-120b")).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("removes a model", () => {
|
|
58
|
-
addExclusion("nvidia/gpt-oss-120b", TEST_FILE);
|
|
59
|
-
addExclusion("xai/grok-4-0709", TEST_FILE);
|
|
60
|
-
removeExclusion("nvidia/gpt-oss-120b", TEST_FILE);
|
|
61
|
-
const list = loadExcludeList(TEST_FILE);
|
|
62
|
-
expect(list.has("nvidia/gpt-oss-120b")).toBe(false);
|
|
63
|
-
expect(list.has("xai/grok-4-0709")).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("clears all exclusions", () => {
|
|
67
|
-
addExclusion("nvidia/gpt-oss-120b", TEST_FILE);
|
|
68
|
-
addExclusion("xai/grok-4-0709", TEST_FILE);
|
|
69
|
-
clearExclusions(TEST_FILE);
|
|
70
|
-
const list = loadExcludeList(TEST_FILE);
|
|
71
|
-
expect(list.size).toBe(0);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("deduplicates entries", () => {
|
|
75
|
-
addExclusion("nvidia/gpt-oss-120b", TEST_FILE);
|
|
76
|
-
addExclusion("nvidia/gpt-oss-120b", TEST_FILE);
|
|
77
|
-
const list = loadExcludeList(TEST_FILE);
|
|
78
|
-
expect(list.size).toBe(1);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("resolves aliases before storing", () => {
|
|
82
|
-
// "free" alias → "nvidia/gpt-oss-120b"
|
|
83
|
-
addExclusion("free", TEST_FILE);
|
|
84
|
-
const list = loadExcludeList(TEST_FILE);
|
|
85
|
-
expect(list.has("nvidia/gpt-oss-120b")).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Step 2: Run test to verify it fails**
|
|
91
|
-
|
|
92
|
-
Run: `npx vitest run src/exclude-models.test.ts`
|
|
93
|
-
Expected: FAIL — module `./exclude-models.js` does not exist
|
|
94
|
-
|
|
95
|
-
**Step 3: Write minimal implementation**
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
// src/exclude-models.ts
|
|
99
|
-
/**
|
|
100
|
-
* Exclude Models — persistent user-configurable model exclusion list.
|
|
101
|
-
*
|
|
102
|
-
* Stores excluded model IDs in ~/.openclaw/blockrun/exclude-models.json.
|
|
103
|
-
* Models in this list are filtered out of routing fallback chains.
|
|
104
|
-
*/
|
|
105
|
-
|
|
106
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
107
|
-
import { dirname, join } from "node:path";
|
|
108
|
-
import { homedir } from "node:os";
|
|
109
|
-
import { resolveModelAlias } from "./models.js";
|
|
110
|
-
|
|
111
|
-
const DEFAULT_EXCLUDE_FILE = join(homedir(), ".openclaw", "blockrun", "exclude-models.json");
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Load the exclude list from disk. Returns empty Set if file missing.
|
|
115
|
-
*/
|
|
116
|
-
export function loadExcludeList(filePath: string = DEFAULT_EXCLUDE_FILE): Set<string> {
|
|
117
|
-
try {
|
|
118
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
119
|
-
const arr = JSON.parse(raw);
|
|
120
|
-
if (Array.isArray(arr)) return new Set(arr);
|
|
121
|
-
return new Set();
|
|
122
|
-
} catch {
|
|
123
|
-
return new Set();
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function save(models: Set<string>, filePath: string): void {
|
|
128
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
129
|
-
writeFileSync(filePath, JSON.stringify([...models].sort(), null, 2) + "\n");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Add a model to the exclude list. Resolves aliases (e.g. "free" → "nvidia/gpt-oss-120b").
|
|
134
|
-
* Returns the resolved model ID.
|
|
135
|
-
*/
|
|
136
|
-
export function addExclusion(model: string, filePath: string = DEFAULT_EXCLUDE_FILE): string {
|
|
137
|
-
const resolved = resolveModelAlias(model);
|
|
138
|
-
const list = loadExcludeList(filePath);
|
|
139
|
-
list.add(resolved);
|
|
140
|
-
save(list, filePath);
|
|
141
|
-
return resolved;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Remove a model from the exclude list. Returns true if it was present.
|
|
146
|
-
*/
|
|
147
|
-
export function removeExclusion(model: string, filePath: string = DEFAULT_EXCLUDE_FILE): boolean {
|
|
148
|
-
const resolved = resolveModelAlias(model);
|
|
149
|
-
const list = loadExcludeList(filePath);
|
|
150
|
-
const had = list.delete(resolved);
|
|
151
|
-
if (had) save(list, filePath);
|
|
152
|
-
return had;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Clear all exclusions.
|
|
157
|
-
*/
|
|
158
|
-
export function clearExclusions(filePath: string = DEFAULT_EXCLUDE_FILE): void {
|
|
159
|
-
save(new Set(), filePath);
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
**Step 4: Run test to verify it passes**
|
|
164
|
-
|
|
165
|
-
Run: `npx vitest run src/exclude-models.test.ts`
|
|
166
|
-
Expected: PASS
|
|
167
|
-
|
|
168
|
-
**Step 5: Commit**
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
git add src/exclude-models.ts src/exclude-models.test.ts
|
|
172
|
-
git commit -m "feat: add exclude-models persistence module"
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
### Task 2: Filter Function in Selector
|
|
178
|
-
|
|
179
|
-
**Files:**
|
|
180
|
-
- Modify: `src/router/selector.ts` — add `filterByExcludeList()`
|
|
181
|
-
- Test: `src/router/selector.test.ts` — add tests
|
|
182
|
-
|
|
183
|
-
**Step 1: Write the failing test**
|
|
184
|
-
|
|
185
|
-
Add to `src/router/selector.test.ts`:
|
|
186
|
-
|
|
187
|
-
```typescript
|
|
188
|
-
import { filterByExcludeList } from "./selector.js";
|
|
189
|
-
|
|
190
|
-
describe("filterByExcludeList", () => {
|
|
191
|
-
it("removes excluded models from chain", () => {
|
|
192
|
-
const chain = ["a/model-1", "b/model-2", "c/model-3"];
|
|
193
|
-
const excluded = new Set(["b/model-2"]);
|
|
194
|
-
expect(filterByExcludeList(chain, excluded)).toEqual(["a/model-1", "c/model-3"]);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("returns original chain if all models excluded (safety net)", () => {
|
|
198
|
-
const chain = ["a/model-1", "b/model-2"];
|
|
199
|
-
const excluded = new Set(["a/model-1", "b/model-2"]);
|
|
200
|
-
expect(filterByExcludeList(chain, excluded)).toEqual(["a/model-1", "b/model-2"]);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("returns original chain for empty exclude set", () => {
|
|
204
|
-
const chain = ["a/model-1", "b/model-2"];
|
|
205
|
-
expect(filterByExcludeList(chain, new Set())).toEqual(["a/model-1", "b/model-2"]);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
**Step 2: Run test to verify it fails**
|
|
211
|
-
|
|
212
|
-
Run: `npx vitest run src/router/selector.test.ts`
|
|
213
|
-
Expected: FAIL — `filterByExcludeList` not exported
|
|
214
|
-
|
|
215
|
-
**Step 3: Write minimal implementation**
|
|
216
|
-
|
|
217
|
-
Add to `src/router/selector.ts`:
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
/**
|
|
221
|
-
* Filter a model list to remove user-excluded models.
|
|
222
|
-
* When all models are excluded, returns the full list as a fallback
|
|
223
|
-
* (same safety pattern as filterByToolCalling/filterByVision).
|
|
224
|
-
*/
|
|
225
|
-
export function filterByExcludeList(models: string[], excludeList: Set<string>): string[] {
|
|
226
|
-
if (excludeList.size === 0) return models;
|
|
227
|
-
const filtered = models.filter((m) => !excludeList.has(m));
|
|
228
|
-
return filtered.length > 0 ? filtered : models;
|
|
229
|
-
}
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
**Step 4: Run test to verify it passes**
|
|
233
|
-
|
|
234
|
-
Run: `npx vitest run src/router/selector.test.ts`
|
|
235
|
-
Expected: PASS
|
|
236
|
-
|
|
237
|
-
**Step 5: Commit**
|
|
238
|
-
|
|
239
|
-
```bash
|
|
240
|
-
git add src/router/selector.ts src/router/selector.test.ts
|
|
241
|
-
git commit -m "feat: add filterByExcludeList to router selector"
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
---
|
|
245
|
-
|
|
246
|
-
### Task 3: Wire Exclude Filter into Proxy Fallback Chain
|
|
247
|
-
|
|
248
|
-
**Files:**
|
|
249
|
-
- Modify: `src/proxy.ts` — add exclude filter step, accept excludeList in ProxyOptions, load at startup
|
|
250
|
-
|
|
251
|
-
**Step 1: Add `excludeModels` to ProxyOptions**
|
|
252
|
-
|
|
253
|
-
In `src/proxy.ts` at line ~1174, add to `ProxyOptions`:
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
/**
|
|
257
|
-
* Set of model IDs to exclude from routing.
|
|
258
|
-
* Excluded models are filtered out of fallback chains.
|
|
259
|
-
* Loaded from ~/.openclaw/blockrun/exclude-models.json
|
|
260
|
-
*/
|
|
261
|
-
excludeModels?: Set<string>;
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
**Step 2: Wire filter into fallback chain building**
|
|
265
|
-
|
|
266
|
-
In `src/proxy.ts` around line 3606 (inside the `if (routingDecision)` block), after the context filter and before the tool-calling filter, add:
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
|
-
// Filter out user-excluded models
|
|
270
|
-
const excludeFiltered = filterByExcludeList(contextFiltered, options.excludeModels ?? new Set());
|
|
271
|
-
const excludeExcluded = contextFiltered.filter((m) => !excludeFiltered.includes(m));
|
|
272
|
-
if (excludeExcluded.length > 0) {
|
|
273
|
-
console.log(
|
|
274
|
-
`[ClawRouter] Exclude filter: excluded ${excludeExcluded.join(", ")} (user preference)`,
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
Then update the next filter to chain from `excludeFiltered` instead of `contextFiltered`:
|
|
280
|
-
|
|
281
|
-
```typescript
|
|
282
|
-
// Change: filterByToolCalling now takes excludeFiltered instead of contextFiltered
|
|
283
|
-
let toolFiltered = filterByToolCalling(excludeFiltered, hasTools, supportsToolCalling);
|
|
284
|
-
const toolExcluded = excludeFiltered.filter((m) => !toolFiltered.includes(m));
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
**Step 3: Also filter the FREE_MODEL fallback at line 3674**
|
|
288
|
-
|
|
289
|
-
Change the free model fallback to respect exclusions:
|
|
290
|
-
|
|
291
|
-
```typescript
|
|
292
|
-
// Ensure free model is the last-resort fallback for non-tool requests — unless user excluded it.
|
|
293
|
-
if (!hasTools && !modelsToTry.includes(FREE_MODEL) && !(options.excludeModels?.has(FREE_MODEL))) {
|
|
294
|
-
modelsToTry.push(FREE_MODEL);
|
|
295
|
-
}
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
**Step 4: Add import for filterByExcludeList**
|
|
299
|
-
|
|
300
|
-
At the top of `proxy.ts`, add `filterByExcludeList` to the selector import:
|
|
301
|
-
|
|
302
|
-
```typescript
|
|
303
|
-
import { selectModel, getFallbackChain, getFallbackChainFiltered, calculateModelCost, filterByToolCalling, filterByVision, filterByExcludeList } from "./router/selector.js";
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
**Step 5: Load exclude list at proxy startup**
|
|
307
|
-
|
|
308
|
-
In `startProxy()` (around line 1426), load the exclude list and pass it through:
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
import { loadExcludeList } from "./exclude-models.js";
|
|
312
|
-
|
|
313
|
-
// Inside startProxy(), before creating the server:
|
|
314
|
-
const excludeModels = options.excludeModels ?? loadExcludeList();
|
|
315
|
-
// Pass excludeModels into the options object used by request handlers
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
Note: Re-read from disk on each request for hot-reload (the file is tiny, cost is negligible):
|
|
319
|
-
|
|
320
|
-
```typescript
|
|
321
|
-
// In the request handler, before building fallback chain:
|
|
322
|
-
const currentExcludeList = loadExcludeList();
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
**Step 6: Run existing tests**
|
|
326
|
-
|
|
327
|
-
Run: `npx vitest run src/proxy.*.test.ts`
|
|
328
|
-
Expected: PASS (existing tests should still pass)
|
|
329
|
-
|
|
330
|
-
**Step 7: Commit**
|
|
331
|
-
|
|
332
|
-
```bash
|
|
333
|
-
git add src/proxy.ts
|
|
334
|
-
git commit -m "feat: wire excludeModels filter into proxy fallback chain"
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
---
|
|
338
|
-
|
|
339
|
-
### Task 4: `/exclude` Telegram Command
|
|
340
|
-
|
|
341
|
-
**Files:**
|
|
342
|
-
- Modify: `src/index.ts` — add `createExcludeCommand()` + register it
|
|
343
|
-
|
|
344
|
-
**Step 1: Create the command function**
|
|
345
|
-
|
|
346
|
-
Add to `src/index.ts` (after `createStatsCommand`):
|
|
347
|
-
|
|
348
|
-
```typescript
|
|
349
|
-
import { loadExcludeList, addExclusion, removeExclusion, clearExclusions } from "./exclude-models.js";
|
|
350
|
-
|
|
351
|
-
async function createExcludeCommand(): Promise<OpenClawPluginCommandDefinition> {
|
|
352
|
-
return {
|
|
353
|
-
name: "exclude",
|
|
354
|
-
description: "Manage excluded models — /exclude add|remove|clear <model>",
|
|
355
|
-
acceptsArgs: true,
|
|
356
|
-
requireAuth: true,
|
|
357
|
-
handler: async (ctx: PluginCommandContext) => {
|
|
358
|
-
const args = ctx.args?.trim() || "";
|
|
359
|
-
const parts = args.split(/\s+/);
|
|
360
|
-
const subcommand = parts[0]?.toLowerCase() || "";
|
|
361
|
-
const modelArg = parts.slice(1).join(" ").trim();
|
|
362
|
-
|
|
363
|
-
// /exclude (no args) — show current list
|
|
364
|
-
if (!subcommand) {
|
|
365
|
-
const list = loadExcludeList();
|
|
366
|
-
if (list.size === 0) {
|
|
367
|
-
return {
|
|
368
|
-
text: "No models excluded.\n\nUsage:\n /exclude add <model> — block a model\n /exclude remove <model> — unblock\n /exclude clear — remove all",
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
const models = [...list].sort().map((m) => ` • ${m}`).join("\n");
|
|
372
|
-
return {
|
|
373
|
-
text: `Excluded models (${list.size}):\n${models}\n\nUse /exclude remove <model> to unblock.`,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// /exclude add <model>
|
|
378
|
-
if (subcommand === "add") {
|
|
379
|
-
if (!modelArg) {
|
|
380
|
-
return { text: "Usage: /exclude add <model>\nExample: /exclude add nvidia/gpt-oss-120b", isError: true };
|
|
381
|
-
}
|
|
382
|
-
const resolved = addExclusion(modelArg);
|
|
383
|
-
const list = loadExcludeList();
|
|
384
|
-
return {
|
|
385
|
-
text: `Excluded: ${resolved}\n\nActive exclusions (${list.size}):\n${[...list].sort().map((m) => ` • ${m}`).join("\n")}`,
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// /exclude remove <model>
|
|
390
|
-
if (subcommand === "remove") {
|
|
391
|
-
if (!modelArg) {
|
|
392
|
-
return { text: "Usage: /exclude remove <model>", isError: true };
|
|
393
|
-
}
|
|
394
|
-
const removed = removeExclusion(modelArg);
|
|
395
|
-
if (!removed) {
|
|
396
|
-
return { text: `Model "${modelArg}" was not in the exclude list.` };
|
|
397
|
-
}
|
|
398
|
-
const list = loadExcludeList();
|
|
399
|
-
return {
|
|
400
|
-
text: `Unblocked: ${modelArg}\n\nActive exclusions (${list.size}):\n${list.size > 0 ? [...list].sort().map((m) => ` • ${m}`).join("\n") : " (none)"}`,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// /exclude clear
|
|
405
|
-
if (subcommand === "clear") {
|
|
406
|
-
clearExclusions();
|
|
407
|
-
return { text: "All model exclusions cleared." };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return {
|
|
411
|
-
text: `Unknown subcommand: ${subcommand}\n\nUsage:\n /exclude — show list\n /exclude add <model>\n /exclude remove <model>\n /exclude clear`,
|
|
412
|
-
isError: true,
|
|
413
|
-
};
|
|
414
|
-
},
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
**Step 2: Register the command**
|
|
420
|
-
|
|
421
|
-
Add after the `/stats` command registration block (~line 971):
|
|
422
|
-
|
|
423
|
-
```typescript
|
|
424
|
-
// Register /exclude command for model exclusion management
|
|
425
|
-
createExcludeCommand()
|
|
426
|
-
.then((excludeCommand) => {
|
|
427
|
-
api.registerCommand(excludeCommand);
|
|
428
|
-
})
|
|
429
|
-
.catch((err) => {
|
|
430
|
-
api.logger.warn(
|
|
431
|
-
`Failed to register /exclude command: ${err instanceof Error ? err.message : String(err)}`,
|
|
432
|
-
);
|
|
433
|
-
});
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
**Step 3: Log active exclusions at startup**
|
|
437
|
-
|
|
438
|
-
In the startup section (after wallet info logging), add:
|
|
439
|
-
|
|
440
|
-
```typescript
|
|
441
|
-
const startupExclusions = loadExcludeList();
|
|
442
|
-
if (startupExclusions.size > 0) {
|
|
443
|
-
api.logger.info(`Model exclusions active (${startupExclusions.size}): ${[...startupExclusions].join(", ")}`);
|
|
444
|
-
}
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
**Step 4: Run all tests**
|
|
448
|
-
|
|
449
|
-
Run: `npx vitest run`
|
|
450
|
-
Expected: PASS
|
|
451
|
-
|
|
452
|
-
**Step 5: Commit**
|
|
453
|
-
|
|
454
|
-
```bash
|
|
455
|
-
git add src/index.ts
|
|
456
|
-
git commit -m "feat: add /exclude command for model exclusion management"
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
---
|
|
460
|
-
|
|
461
|
-
### Task 5: Integration Test
|
|
462
|
-
|
|
463
|
-
**Files:**
|
|
464
|
-
- Create: `src/exclude-models.integration.test.ts`
|
|
465
|
-
|
|
466
|
-
**Step 1: Write integration test**
|
|
467
|
-
|
|
468
|
-
```typescript
|
|
469
|
-
// src/exclude-models.integration.test.ts
|
|
470
|
-
import { describe, it, expect } from "vitest";
|
|
471
|
-
import { filterByExcludeList } from "./router/selector.js";
|
|
472
|
-
import { DEFAULT_ROUTING_CONFIG } from "./router/config.js";
|
|
473
|
-
import { getFallbackChain } from "./router/selector.js";
|
|
474
|
-
|
|
475
|
-
describe("excludeModels integration", () => {
|
|
476
|
-
it("filters nvidia/gpt-oss-120b from eco SIMPLE chain", () => {
|
|
477
|
-
const chain = getFallbackChain("SIMPLE", DEFAULT_ROUTING_CONFIG.ecoTiers!);
|
|
478
|
-
const excluded = new Set(["nvidia/gpt-oss-120b"]);
|
|
479
|
-
const filtered = filterByExcludeList(chain, excluded);
|
|
480
|
-
|
|
481
|
-
expect(filtered).not.toContain("nvidia/gpt-oss-120b");
|
|
482
|
-
expect(filtered.length).toBeGreaterThan(0); // safety: still has models
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it("excludes multiple models across eco tiers", () => {
|
|
486
|
-
const exclude = new Set(["nvidia/gpt-oss-120b", "xai/grok-4-0709"]);
|
|
487
|
-
|
|
488
|
-
for (const tier of ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"] as const) {
|
|
489
|
-
const chain = getFallbackChain(tier, DEFAULT_ROUTING_CONFIG.ecoTiers!);
|
|
490
|
-
const filtered = filterByExcludeList(chain, exclude);
|
|
491
|
-
for (const model of exclude) {
|
|
492
|
-
if (chain.includes(model)) {
|
|
493
|
-
// Only check if the model was in the chain to begin with
|
|
494
|
-
expect(filtered).not.toContain(model);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
expect(filtered.length).toBeGreaterThan(0);
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
it("gracefully handles excluding ALL models in a tier", () => {
|
|
502
|
-
const chain = getFallbackChain("SIMPLE", DEFAULT_ROUTING_CONFIG.ecoTiers!);
|
|
503
|
-
const excludeAll = new Set(chain);
|
|
504
|
-
const filtered = filterByExcludeList(chain, excludeAll);
|
|
505
|
-
// Safety net: returns original chain when all excluded
|
|
506
|
-
expect(filtered).toEqual(chain);
|
|
507
|
-
});
|
|
508
|
-
});
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
**Step 2: Run integration test**
|
|
512
|
-
|
|
513
|
-
Run: `npx vitest run src/exclude-models.integration.test.ts`
|
|
514
|
-
Expected: PASS
|
|
515
|
-
|
|
516
|
-
**Step 3: Run full test suite**
|
|
517
|
-
|
|
518
|
-
Run: `npx vitest run`
|
|
519
|
-
Expected: ALL PASS
|
|
520
|
-
|
|
521
|
-
**Step 4: Commit**
|
|
522
|
-
|
|
523
|
-
```bash
|
|
524
|
-
git add src/exclude-models.integration.test.ts
|
|
525
|
-
git commit -m "test: add exclude-models integration tests"
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
---
|
|
529
|
-
|
|
530
|
-
### Summary
|
|
531
|
-
|
|
532
|
-
| Task | What | Files |
|
|
533
|
-
|------|------|-------|
|
|
534
|
-
| 1 | Persistence module (load/add/remove/clear) | `src/exclude-models.ts`, test |
|
|
535
|
-
| 2 | `filterByExcludeList()` in selector | `src/router/selector.ts`, test |
|
|
536
|
-
| 3 | Wire into proxy fallback chain | `src/proxy.ts` |
|
|
537
|
-
| 4 | `/exclude` Telegram command | `src/index.ts` |
|
|
538
|
-
| 5 | Integration tests | `src/exclude-models.integration.test.ts` |
|
package/docs/vs-openrouter.md
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
# ClawRouter vs OpenRouter
|
|
2
|
-
|
|
3
|
-
OpenRouter is a popular LLM routing service. Here's why ClawRouter is built differently — and why it matters for agents.
|
|
4
|
-
|
|
5
|
-
## TL;DR
|
|
6
|
-
|
|
7
|
-
**OpenRouter is built for developers. ClawRouter is built for agents.**
|
|
8
|
-
|
|
9
|
-
| Aspect | OpenRouter | ClawRouter |
|
|
10
|
-
| ------------------ | ------------------------------------- | -------------------------------------- |
|
|
11
|
-
| **Setup** | Human creates account, pastes API key | Agent generates wallet, receives funds |
|
|
12
|
-
| **Authentication** | API key (shared secret) | Wallet signature (cryptographic) |
|
|
13
|
-
| **Payment** | Prepaid balance (custodial) | Per-request USDC (non-custodial) |
|
|
14
|
-
| **Routing** | Server-side, proprietary | Client-side, open source, <1ms |
|
|
15
|
-
| **Rate limits** | Per-key quotas | None (your wallet, your limits) |
|
|
16
|
-
| **Empty balance** | Request fails | Auto-fallback to free tier |
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## The Problem with API Keys
|
|
21
|
-
|
|
22
|
-
OpenRouter (and every traditional LLM gateway) uses API keys for authentication. This breaks agent autonomy:
|
|
23
|
-
|
|
24
|
-
### 1. Key Leakage in LLM Context
|
|
25
|
-
|
|
26
|
-
**OpenClaw Issue [#11202](https://github.com/openclaw/openclaw/issues/11202)**: API keys configured in `openclaw.json` are resolved and serialized into every LLM request payload. Every provider sees every other provider's keys.
|
|
27
|
-
|
|
28
|
-
> "OpenRouter sees your NVIDIA key, Anthropic sees your Google key... keys are sent on every turn."
|
|
29
|
-
|
|
30
|
-
**ClawRouter**: No API keys. Authentication happens via cryptographic wallet signatures. There's nothing to leak because there are no shared secrets.
|
|
31
|
-
|
|
32
|
-
### 2. Rate Limit Hell
|
|
33
|
-
|
|
34
|
-
**OpenClaw Issue [#8615](https://github.com/openclaw/openclaw/issues/8615)**: Single API key support means heavy users hit rate limits (429 errors) quickly. Users request multi-key load balancing, but that's just patching a broken model.
|
|
35
|
-
|
|
36
|
-
**ClawRouter**: Non-custodial wallets. You control your own keys. No shared rate limits. Scale by funding more wallets if needed.
|
|
37
|
-
|
|
38
|
-
### 3. Setup Friction
|
|
39
|
-
|
|
40
|
-
**OpenClaw Issues [#16257](https://github.com/openclaw/openclaw/issues/16257), [#16226](https://github.com/openclaw/openclaw/issues/16226)**: Latest installer skips model selection, shows "No auth configured for provider anthropic". Users can't even get started without debugging config.
|
|
41
|
-
|
|
42
|
-
**ClawRouter**: One-line install. 30+ models auto-configured. No API keys to paste.
|
|
43
|
-
|
|
44
|
-
### 4. Model Path Collision
|
|
45
|
-
|
|
46
|
-
**OpenClaw Issue [#2373](https://github.com/openclaw/openclaw/issues/2373)**: `openrouter/auto` is broken because OpenClaw prefixes all OpenRouter models with `openrouter/`, so the actual model becomes `openrouter/openrouter/auto`.
|
|
47
|
-
|
|
48
|
-
**ClawRouter**: Clean namespace. `blockrun/auto` just works. No prefix collision.
|
|
49
|
-
|
|
50
|
-
### 5. False Billing Errors
|
|
51
|
-
|
|
52
|
-
**OpenClaw Issue [#16237](https://github.com/openclaw/openclaw/issues/16237)**: The regex `/\b402\b/` falsely matches normal content (e.g., "402 calories") as a billing error, replacing valid AI responses with error messages.
|
|
53
|
-
|
|
54
|
-
**ClawRouter**: Native x402 protocol support. Precise error handling. No regex hacks.
|
|
55
|
-
|
|
56
|
-
### 6. Unknown Model Failures
|
|
57
|
-
|
|
58
|
-
**OpenClaw Issues [#16277](https://github.com/openclaw/openclaw/issues/16277), [#10687](https://github.com/openclaw/openclaw/issues/10687)**: Static model catalog causes "Unknown model" errors when providers add new models or during sub-agent spawns.
|
|
59
|
-
|
|
60
|
-
**ClawRouter**: 30+ models pre-configured, auto-updated catalog.
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## Agent-Native: Why It Matters
|
|
65
|
-
|
|
66
|
-
Traditional LLM gateways require a human in the loop:
|
|
67
|
-
|
|
68
|
-
```
|
|
69
|
-
Traditional Flow (Human-in-the-loop):
|
|
70
|
-
Human → creates account → gets API key → pastes into config → agent runs
|
|
71
|
-
|
|
72
|
-
Agent-Native Flow (Fully autonomous):
|
|
73
|
-
Agent → generates wallet → receives USDC → pays per request → runs
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
| Capability | OpenRouter | ClawRouter |
|
|
77
|
-
| -------------------- | ----------------------- | -------------------------- |
|
|
78
|
-
| **Account creation** | Requires human | Agent generates wallet |
|
|
79
|
-
| **Authentication** | Shared secret (API key) | Cryptographic signature |
|
|
80
|
-
| **Payment** | Human prepays balance | Agent pays per request |
|
|
81
|
-
| **Funds custody** | They hold your money | You hold your keys |
|
|
82
|
-
| **Empty balance** | Request fails | Auto-fallback to free tier |
|
|
83
|
-
|
|
84
|
-
### The x402 Difference
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
Request → 402 Response (price: $0.003)
|
|
88
|
-
→ Agent's wallet signs payment
|
|
89
|
-
→ Response delivered
|
|
90
|
-
|
|
91
|
-
No accounts. No API keys. No human intervention.
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
**Agents can:**
|
|
95
|
-
|
|
96
|
-
- Spawn with a fresh wallet
|
|
97
|
-
- Receive funds programmatically
|
|
98
|
-
- Pay for exactly what they use
|
|
99
|
-
- Never trust a third party with their funds
|
|
100
|
-
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
## Routing: Cloud vs Local
|
|
104
|
-
|
|
105
|
-
### OpenRouter
|
|
106
|
-
|
|
107
|
-
- Routing decisions happen on OpenRouter's servers
|
|
108
|
-
- You trust their proprietary algorithm
|
|
109
|
-
- No visibility into why a model was chosen
|
|
110
|
-
- Adds latency for every request
|
|
111
|
-
|
|
112
|
-
### ClawRouter
|
|
113
|
-
|
|
114
|
-
- **100% local routing** — 15-dimension weighted scoring runs on YOUR machine
|
|
115
|
-
- **<1ms decisions** — no API calls for routing
|
|
116
|
-
- **Open source** — inspect the exact scoring logic in [`src/router.ts`](../src/router.ts)
|
|
117
|
-
- **Transparent** — see why each model is chosen
|
|
118
|
-
|
|
119
|
-
---
|
|
120
|
-
|
|
121
|
-
## Quick Start
|
|
122
|
-
|
|
123
|
-
Already using OpenRouter? Switch in 60 seconds:
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
# 1. Install ClawRouter
|
|
127
|
-
curl -fsSL https://blockrun.ai/ClawRouter-update | bash
|
|
128
|
-
|
|
129
|
-
# 2. Restart gateway
|
|
130
|
-
openclaw gateway restart
|
|
131
|
-
|
|
132
|
-
# 3. Fund wallet (address shown during install)
|
|
133
|
-
# $5 USDC on Base = thousands of requests
|
|
134
|
-
|
|
135
|
-
# 4. Switch model
|
|
136
|
-
/model blockrun/auto
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
Your OpenRouter config stays intact — ClawRouter is additive, not replacement.
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
## Summary
|
|
144
|
-
|
|
145
|
-
> **OpenRouter**: Built for developers who paste API keys
|
|
146
|
-
>
|
|
147
|
-
> **ClawRouter**: Built for agents that manage their own wallets
|
|
148
|
-
|
|
149
|
-
The future of AI isn't humans configuring API keys. It's agents autonomously acquiring and paying for resources.
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
<div align="center">
|
|
154
|
-
|
|
155
|
-
**Questions?** [Telegram](https://t.me/blockrunAI) · [X](https://x.com/BlockRunAI) · [GitHub](https://github.com/BlockRunAI/ClawRouter)
|
|
156
|
-
|
|
157
|
-
</div>
|