@blockrun/clawrouter 0.12.62 → 0.12.63
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/docs/anthropic-cost-savings.md +349 -0
- package/docs/architecture.md +559 -0
- 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/blog-benchmark-2026-03.md +184 -0
- package/docs/blog-openclaw-cost-overruns.md +197 -0
- package/docs/clawrouter-savings.png +0 -0
- package/docs/configuration.md +512 -0
- package/docs/features.md +257 -0
- package/docs/image-generation.md +380 -0
- package/docs/plans/2026-02-03-smart-routing-design.md +267 -0
- package/docs/plans/2026-02-13-e2e-docker-deployment.md +1260 -0
- package/docs/plans/2026-02-28-worker-network.md +947 -0
- package/docs/plans/2026-03-18-error-classification.md +574 -0
- package/docs/plans/2026-03-19-exclude-models.md +538 -0
- package/docs/routing-profiles.md +81 -0
- package/docs/subscription-failover.md +320 -0
- package/docs/technical-routing-2026-03.md +322 -0
- package/docs/troubleshooting.md +159 -0
- package/docs/vision.md +49 -0
- package/docs/vs-openrouter.md +157 -0
- package/docs/worker-network.md +1241 -0
- package/package.json +2 -1
|
@@ -0,0 +1,538 @@
|
|
|
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` |
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Routing Profiles & Pricing
|
|
2
|
+
|
|
3
|
+
ClawRouter offers four routing profiles to balance cost vs quality. Prices are in **$/M tokens** (input/output).
|
|
4
|
+
|
|
5
|
+
## ECO (Absolute Cheapest)
|
|
6
|
+
|
|
7
|
+
Use `blockrun/eco` for maximum cost savings.
|
|
8
|
+
|
|
9
|
+
| Tier | Primary Model | Input | Output |
|
|
10
|
+
| --------- | ---------------------------- | ----- | ------ |
|
|
11
|
+
| SIMPLE | nvidia/gpt-oss-120b | $0.00 | $0.00 |
|
|
12
|
+
| MEDIUM | google/gemini-2.5-flash-lite | $0.10 | $0.40 |
|
|
13
|
+
| COMPLEX | google/gemini-2.5-flash-lite | $0.10 | $0.40 |
|
|
14
|
+
| REASONING | xai/grok-4-1-fast-reasoning | $0.20 | $0.50 |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## AUTO (Balanced - Default)
|
|
19
|
+
|
|
20
|
+
Use `blockrun/auto` for the best quality/price balance.
|
|
21
|
+
|
|
22
|
+
| Tier | Primary Model | Input | Output |
|
|
23
|
+
| --------- | ----------------------------- | ----- | ------ |
|
|
24
|
+
| SIMPLE | moonshot/kimi-k2.5 | $0.60 | $3.00 |
|
|
25
|
+
| MEDIUM | xai/grok-code-fast-1 | $0.20 | $1.50 |
|
|
26
|
+
| COMPLEX | google/gemini-3.1-pro | $2.00 | $12.00 |
|
|
27
|
+
| REASONING | xai/grok-4-1-fast-reasoning | $0.20 | $0.50 |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## PREMIUM (Best Quality)
|
|
32
|
+
|
|
33
|
+
Use `blockrun/premium` for maximum quality.
|
|
34
|
+
|
|
35
|
+
| Tier | Primary Model | Input | Output |
|
|
36
|
+
| --------- | -------------------- | ----- | ------ |
|
|
37
|
+
| SIMPLE | moonshot/kimi-k2.5 | $0.60 | $3.00 |
|
|
38
|
+
| MEDIUM | openai/gpt-5.2-codex | $1.75 | $14.00 |
|
|
39
|
+
| COMPLEX | claude-opus-4.6 | $5.00 | $25.00 |
|
|
40
|
+
| REASONING | claude-sonnet-4.6 | $3.00 | $15.00 |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## AGENTIC (Multi-Step Tasks)
|
|
45
|
+
|
|
46
|
+
Use `blockrun/agentic` for autonomous multi-step tasks, or let ClawRouter auto-detect agentic patterns.
|
|
47
|
+
|
|
48
|
+
| Tier | Primary Model | Input | Output |
|
|
49
|
+
| --------- | -------------------- | ----- | ------ |
|
|
50
|
+
| SIMPLE | moonshot/kimi-k2.5 | $0.60 | $3.00 |
|
|
51
|
+
| MEDIUM | xai/grok-code-fast-1 | $0.20 | $1.50 |
|
|
52
|
+
| COMPLEX | claude-sonnet-4.6 | $3.00 | $15.00 |
|
|
53
|
+
| REASONING | claude-sonnet-4.6 | $3.00 | $15.00 |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## ECO vs AUTO Savings
|
|
58
|
+
|
|
59
|
+
| Tier | ECO | AUTO | Savings |
|
|
60
|
+
| --------- | ----- | ------ | -------- |
|
|
61
|
+
| SIMPLE | FREE | $3.60 | **100%** |
|
|
62
|
+
| MEDIUM | $0.50 | $1.70 | **71%** |
|
|
63
|
+
| COMPLEX | $0.50 | $14.00 | **96%** |
|
|
64
|
+
| REASONING | $0.70 | $0.70 | 0% |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## How Tiers Work
|
|
69
|
+
|
|
70
|
+
ClawRouter automatically classifies your query into one of four tiers:
|
|
71
|
+
|
|
72
|
+
- **SIMPLE**: Basic questions, short responses, simple lookups
|
|
73
|
+
- **MEDIUM**: Code generation, moderate complexity tasks
|
|
74
|
+
- **COMPLEX**: Large context, multi-step reasoning, complex code
|
|
75
|
+
- **REASONING**: Logic puzzles, math, chain-of-thought tasks
|
|
76
|
+
|
|
77
|
+
The router picks the cheapest model capable of handling your query's tier.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
_Last updated: v0.12.24_
|