@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.
Files changed (34) hide show
  1. package/README.md +55 -55
  2. package/dist/cli.js +50 -14
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +57 -16
  5. package/dist/index.js.map +1 -1
  6. package/docs/anthropic-cost-savings.md +90 -85
  7. package/docs/architecture.md +12 -12
  8. package/docs/{blog-openclaw-cost-overruns.md → clawrouter-cuts-llm-api-costs-500x.md} +27 -27
  9. package/docs/clawrouter-vs-openrouter-llm-routing-comparison.md +280 -0
  10. package/docs/configuration.md +2 -2
  11. package/docs/image-generation.md +39 -39
  12. package/docs/{blog-benchmark-2026-03.md → llm-router-benchmark-46-models-sub-1ms-routing.md} +61 -64
  13. package/docs/routing-profiles.md +6 -6
  14. package/docs/{technical-routing-2026-03.md → smart-llm-router-14-dimension-classifier.md} +29 -28
  15. package/docs/worker-network.md +438 -347
  16. package/package.json +3 -2
  17. package/scripts/reinstall.sh +31 -6
  18. package/scripts/update.sh +6 -1
  19. package/docs/assets/blockrun-248-day-cost-overrun-problem.png +0 -0
  20. package/docs/assets/blockrun-clawrouter-7-layer-token-compression-openclaw.png +0 -0
  21. package/docs/assets/blockrun-clawrouter-observation-compression-97-percent-token-savings.png +0 -0
  22. package/docs/assets/blockrun-clawrouter-openclaw-agentic-proxy-architecture.png +0 -0
  23. package/docs/assets/blockrun-clawrouter-openclaw-automatic-tier-routing-model-selection.png +0 -0
  24. package/docs/assets/blockrun-clawrouter-openclaw-error-classification-retry-storm-prevention.png +0 -0
  25. package/docs/assets/blockrun-clawrouter-openclaw-session-memory-journaling-vs-context-compounding.png +0 -0
  26. package/docs/assets/blockrun-clawrouter-vs-openclaw-standalone-comparison-production-safety.png +0 -0
  27. package/docs/assets/blockrun-clawrouter-x402-usdc-micropayment-wallet-budget-control.png +0 -0
  28. package/docs/assets/blockrun-openclaw-inference-layer-blind-spots.png +0 -0
  29. package/docs/plans/2026-02-03-smart-routing-design.md +0 -267
  30. package/docs/plans/2026-02-13-e2e-docker-deployment.md +0 -1260
  31. package/docs/plans/2026-02-28-worker-network.md +0 -947
  32. package/docs/plans/2026-03-18-error-classification.md +0 -574
  33. package/docs/plans/2026-03-19-exclude-models.md +0 -538
  34. package/docs/vs-openrouter.md +0 -157
@@ -1,574 +0,0 @@
1
- # Error Classification Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Replace ClawRouter's binary `isProviderError` with per-category error classification so that 401 auth failures never pollute the 429 rate-limit cooldown map, each provider's error state is isolated, and `/stats` exposes per-provider error breakdowns.
6
-
7
- **Architecture:** Add `ErrorCategory` type + `categorizeError()` function in `proxy.ts`. Extend `ModelRequestResult` to carry the category. Replace the flat `rateLimitedModels`-only tracking with a dual-map (rate-limit + overloaded) plus an in-memory `perProviderErrors` counter. The fallback loop switches on category instead of raw status code. `/stats` merges the runtime map into its JSON response.
8
-
9
- **Tech Stack:** TypeScript, Node.js HTTP server, existing proxy.ts patterns. Tests use Bun's native test runner (`bun test`).
10
-
11
- ---
12
-
13
- ## Task 1: Add `ErrorCategory` type and `categorizeError()` function
14
-
15
- **Files:**
16
- - Modify: `src/proxy.ts` (near line 330, before `rateLimitedModels`)
17
-
18
- **Step 1: Locate insertion point**
19
-
20
- In `src/proxy.ts`, find the comment block at line ~330:
21
- ```
22
- /** Track rate-limited models to avoid hitting them again. */
23
- const rateLimitedModels = ...
24
- ```
25
- Insert the new code BEFORE this block.
26
-
27
- **Step 2: Insert `ErrorCategory` type and `categorizeError()`**
28
-
29
- ```typescript
30
- /**
31
- * Semantic error categories from upstream provider responses.
32
- * Used to distinguish auth failures from rate limits from server errors
33
- * so each category can be handled independently without cross-contamination.
34
- */
35
- export type ErrorCategory =
36
- | "auth_failure" // 401, 403: Wrong key or forbidden — don't retry with same key
37
- | "quota_exceeded" // 403 with plan/quota body: Plan limit hit
38
- | "rate_limited" // 429: Actual throttling — 60s cooldown
39
- | "overloaded" // 529, 503+overload body: Provider capacity — 15s cooldown
40
- | "server_error" // 5xx general: Transient — fallback immediately
41
- | "payment_error" // 402: x402 payment or funds issue
42
- | "config_error"; // 400, 413: Bad request content — skip this model
43
-
44
- /**
45
- * Classify an upstream error response into a semantic category.
46
- * Returns null if the status+body is not a provider-side issue worth retrying.
47
- */
48
- export function categorizeError(status: number, body: string): ErrorCategory | null {
49
- if (status === 401) return "auth_failure";
50
- if (status === 402) return "payment_error";
51
- if (status === 403) {
52
- if (/plan.*limit|quota.*exceeded|subscription|allowance/i.test(body))
53
- return "quota_exceeded";
54
- return "auth_failure"; // generic 403 = forbidden = likely auth issue
55
- }
56
- if (status === 429) return "rate_limited";
57
- if (status === 529) return "overloaded";
58
- if (status === 503 && /overload|capacity|too.*many.*request/i.test(body)) return "overloaded";
59
- if (status >= 500) return "server_error";
60
- if (status === 400 || status === 413) {
61
- // Only fallback on content-size or billing patterns; bare 400 = our bug, don't cycle
62
- if (PROVIDER_ERROR_PATTERNS.some((p) => p.test(body))) return "config_error";
63
- return null;
64
- }
65
- return null;
66
- }
67
- ```
68
-
69
- **Step 3: Verify it compiles**
70
-
71
- ```bash
72
- cd /Users/vickyfu/Documents/blockrun-web/ClawRouter && bun run build 2>&1 | tail -5
73
- ```
74
- Expected: no TypeScript errors.
75
-
76
- **Step 4: Commit**
77
-
78
- ```bash
79
- git add src/proxy.ts
80
- git commit -m "feat: add ErrorCategory type and categorizeError() function"
81
- ```
82
-
83
- ---
84
-
85
- ## Task 2: Add `OVERLOAD_COOLDOWN_MS`, `overloadedModels` tracking, and `perProviderErrors` counter
86
-
87
- **Files:**
88
- - Modify: `src/proxy.ts` (near line 116 for constant, and after `markRateLimited` for new functions)
89
-
90
- **Step 1: Add `OVERLOAD_COOLDOWN_MS` constant**
91
-
92
- Near line 116 (next to `RATE_LIMIT_COOLDOWN_MS`), add:
93
- ```typescript
94
- const OVERLOAD_COOLDOWN_MS = 15_000; // 15 seconds cooldown for overloaded providers
95
- ```
96
-
97
- **Step 2: Add `ProviderErrorCounts` type and `perProviderErrors` map**
98
-
99
- After `const rateLimitedModels = new Map<string, number>();` (line ~334), add:
100
-
101
- ```typescript
102
- /** Per-model overload tracking (529/503 capacity errors) — shorter cooldown than rate limits. */
103
- const overloadedModels = new Map<string, number>();
104
-
105
- /** Per-model error category counts (in-memory, resets on restart). */
106
- type ProviderErrorCounts = {
107
- auth_failure: number;
108
- quota_exceeded: number;
109
- rate_limited: number;
110
- overloaded: number;
111
- server_error: number;
112
- payment_error: number;
113
- config_error: number;
114
- };
115
- const perProviderErrors = new Map<string, ProviderErrorCounts>();
116
-
117
- /** Record an error category hit for a model. */
118
- function recordProviderError(modelId: string, category: ErrorCategory): void {
119
- if (!perProviderErrors.has(modelId)) {
120
- perProviderErrors.set(modelId, {
121
- auth_failure: 0,
122
- quota_exceeded: 0,
123
- rate_limited: 0,
124
- overloaded: 0,
125
- server_error: 0,
126
- payment_error: 0,
127
- config_error: 0,
128
- });
129
- }
130
- perProviderErrors.get(modelId)![category]++;
131
- }
132
- ```
133
-
134
- **Step 3: Add `markOverloaded()` and `isOverloaded()` functions**
135
-
136
- After the existing `markRateLimited()` function (line ~357), add:
137
-
138
- ```typescript
139
- /**
140
- * Mark a model as temporarily overloaded (529/503 capacity).
141
- * Shorter cooldown than rate limits since capacity restores quickly.
142
- */
143
- function markOverloaded(modelId: string): void {
144
- overloadedModels.set(modelId, Date.now());
145
- console.log(`[ClawRouter] Model ${modelId} overloaded, will deprioritize for 15s`);
146
- }
147
-
148
- /** Check if a model is in its overload cooldown period. */
149
- function isOverloaded(modelId: string): boolean {
150
- const hitTime = overloadedModels.get(modelId);
151
- if (!hitTime) return false;
152
- if (Date.now() - hitTime >= OVERLOAD_COOLDOWN_MS) {
153
- overloadedModels.delete(modelId);
154
- return false;
155
- }
156
- return true;
157
- }
158
- ```
159
-
160
- **Step 4: Update `prioritizeNonRateLimited` to also exclude overloaded models**
161
-
162
- Find the existing `prioritizeNonRateLimited` function (line ~362) and update it:
163
-
164
- OLD:
165
- ```typescript
166
- function prioritizeNonRateLimited(models: string[]): string[] {
167
- const available: string[] = [];
168
- const rateLimited: string[] = [];
169
-
170
- for (const model of models) {
171
- if (isRateLimited(model)) {
172
- rateLimited.push(model);
173
- } else {
174
- available.push(model);
175
- }
176
- }
177
-
178
- return [...available, ...rateLimited];
179
- }
180
- ```
181
-
182
- NEW:
183
- ```typescript
184
- function prioritizeNonRateLimited(models: string[]): string[] {
185
- const available: string[] = [];
186
- const degraded: string[] = [];
187
-
188
- for (const model of models) {
189
- if (isRateLimited(model) || isOverloaded(model)) {
190
- degraded.push(model);
191
- } else {
192
- available.push(model);
193
- }
194
- }
195
-
196
- return [...available, ...degraded];
197
- }
198
- ```
199
-
200
- **Step 5: Build to verify**
201
-
202
- ```bash
203
- cd /Users/vickyfu/Documents/blockrun-web/ClawRouter && bun run build 2>&1 | tail -5
204
- ```
205
-
206
- **Step 6: Commit**
207
-
208
- ```bash
209
- git add src/proxy.ts
210
- git commit -m "feat: add overload tracking and per-provider error counters"
211
- ```
212
-
213
- ---
214
-
215
- ## Task 3: Thread `errorCategory` through `ModelRequestResult`
216
-
217
- **Files:**
218
- - Modify: `src/proxy.ts` (lines ~2077-2205)
219
-
220
- **Step 1: Update `ModelRequestResult` type**
221
-
222
- Find the type definition (line ~2077):
223
- ```typescript
224
- type ModelRequestResult = {
225
- success: boolean;
226
- response?: Response;
227
- errorBody?: string;
228
- errorStatus?: number;
229
- isProviderError?: boolean;
230
- };
231
- ```
232
-
233
- Replace with:
234
- ```typescript
235
- type ModelRequestResult = {
236
- success: boolean;
237
- response?: Response;
238
- errorBody?: string;
239
- errorStatus?: number;
240
- isProviderError?: boolean;
241
- errorCategory?: ErrorCategory; // Semantic error classification
242
- };
243
- ```
244
-
245
- **Step 2: Update `tryModelRequest` to set `errorCategory`**
246
-
247
- Find the block in `tryModelRequest` that currently does (line ~2159):
248
- ```typescript
249
- const isProviderErr = isProviderError(response.status, errorBody);
250
-
251
- return {
252
- success: false,
253
- errorBody,
254
- errorStatus: response.status,
255
- isProviderError: isProviderErr,
256
- };
257
- ```
258
-
259
- Replace with:
260
- ```typescript
261
- const category = categorizeError(response.status, errorBody);
262
-
263
- return {
264
- success: false,
265
- errorBody,
266
- errorStatus: response.status,
267
- isProviderError: category !== null,
268
- errorCategory: category ?? undefined,
269
- };
270
- ```
271
-
272
- Note: This removes the call to the now-redundant `isProviderError()` function. The function itself can stay (it's referenced by degraded-response checks for body patterns).
273
-
274
- **Step 3: Build to verify**
275
-
276
- ```bash
277
- cd /Users/vickyfu/Documents/blockrun-web/ClawRouter && bun run build 2>&1 | tail -5
278
- ```
279
-
280
- **Step 4: Commit**
281
-
282
- ```bash
283
- git add src/proxy.ts
284
- git commit -m "feat: thread errorCategory through ModelRequestResult"
285
- ```
286
-
287
- ---
288
-
289
- ## Task 4: Update fallback loop to act on error category
290
-
291
- **Files:**
292
- - Modify: `src/proxy.ts` (lines ~3463-3500)
293
-
294
- **Step 1: Find the fallback loop's error handling block**
295
-
296
- Find this code (around line 3463):
297
- ```typescript
298
- // Track 429 rate limits to deprioritize this model for future requests
299
- if (result.errorStatus === 429) {
300
- markRateLimited(tryModel);
301
- // Check for server-side update hint
302
- try {
303
- const parsed = JSON.parse(result.errorBody || "{}");
304
- if (parsed.update_available) {
305
- // ... update hint logging
306
- }
307
- } catch {
308
- /* ignore parse errors */
309
- }
310
- }
311
- ```
312
-
313
- **Step 2: Replace with category-based handling**
314
-
315
- Replace the entire block (from the `// Track 429` comment up to but NOT including the `// Payment error` comment) with:
316
-
317
- ```typescript
318
- // Record error and apply category-specific handling
319
- const errorCat = result.errorCategory;
320
- if (errorCat) {
321
- recordProviderError(tryModel, errorCat);
322
- }
323
-
324
- if (errorCat === "rate_limited") {
325
- markRateLimited(tryModel);
326
- // Check for server-side update hint in 429 response
327
- try {
328
- const parsed = JSON.parse(result.errorBody || "{}");
329
- if (parsed.update_available) {
330
- console.log("");
331
- console.log(
332
- `\x1b[33m⬆️ ClawRouter ${parsed.update_available} available (you have ${VERSION})\x1b[0m`,
333
- );
334
- console.log(
335
- ` Run: \x1b[36mcurl -fsSL ${parsed.update_url || "https://blockrun.ai/ClawRouter-update"} | bash\x1b[0m`,
336
- );
337
- console.log("");
338
- }
339
- } catch {
340
- /* ignore parse errors */
341
- }
342
- } else if (errorCat === "overloaded") {
343
- markOverloaded(tryModel);
344
- } else if (errorCat === "auth_failure" || errorCat === "quota_exceeded") {
345
- console.log(
346
- `[ClawRouter] 🔑 ${errorCat === "auth_failure" ? "Auth failure" : "Quota exceeded"} for ${tryModel} — check provider config`,
347
- );
348
- }
349
- ```
350
-
351
- **Step 3: Build to verify**
352
-
353
- ```bash
354
- cd /Users/vickyfu/Documents/blockrun-web/ClawRouter && bun run build 2>&1 | tail -5
355
- ```
356
-
357
- **Step 4: Commit**
358
-
359
- ```bash
360
- git add src/proxy.ts
361
- git commit -m "feat: category-based error handling in fallback loop"
362
- ```
363
-
364
- ---
365
-
366
- ## Task 5: Expose `providerErrors` in `/stats` response
367
-
368
- **Files:**
369
- - Modify: `src/proxy.ts` (lines ~1567-1587)
370
-
371
- **Step 1: Find the `/stats` GET handler**
372
-
373
- Find this code (around line 1567):
374
- ```typescript
375
- if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
376
- try {
377
- const url = new URL(req.url, "http://localhost");
378
- const days = parseInt(url.searchParams.get("days") || "7", 10);
379
- const stats = await getStats(Math.min(days, 30));
380
-
381
- res.writeHead(200, {
382
- "Content-Type": "application/json",
383
- "Cache-Control": "no-cache",
384
- });
385
- res.end(JSON.stringify(stats, null, 2));
386
- ```
387
-
388
- **Step 2: Augment response with runtime error counts**
389
-
390
- Replace `res.end(JSON.stringify(stats, null, 2));` with:
391
-
392
- ```typescript
393
- res.end(
394
- JSON.stringify(
395
- {
396
- ...stats,
397
- providerErrors: Object.fromEntries(perProviderErrors),
398
- },
399
- null,
400
- 2,
401
- ),
402
- );
403
- ```
404
-
405
- This adds a `providerErrors` field to the JSON response, e.g.:
406
- ```json
407
- {
408
- "providerErrors": {
409
- "openai/gpt-4o": { "auth_failure": 0, "rate_limited": 2, "overloaded": 1, "server_error": 0, "payment_error": 0, "config_error": 0, "quota_exceeded": 0 },
410
- "anthropic/claude-3-7-sonnet": { "auth_failure": 1, "rate_limited": 0, "overloaded": 0, "server_error": 3, "payment_error": 0, "config_error": 0, "quota_exceeded": 0 }
411
- }
412
- }
413
- ```
414
-
415
- **Step 3: Build to verify**
416
-
417
- ```bash
418
- cd /Users/vickyfu/Documents/blockrun-web/ClawRouter && bun run build 2>&1 | tail -5
419
- ```
420
-
421
- **Step 4: Commit**
422
-
423
- ```bash
424
- git add src/proxy.ts
425
- git commit -m "feat: expose per-provider error stats in /stats endpoint"
426
- ```
427
-
428
- ---
429
-
430
- ## Task 6: Write tests for `categorizeError()`
431
-
432
- **Files:**
433
- - Create: `src/error-classification.test.ts`
434
-
435
- **Step 1: Create test file**
436
-
437
- ```typescript
438
- import { describe, it, expect } from "bun:test";
439
- import { categorizeError } from "./proxy.js";
440
-
441
- describe("categorizeError", () => {
442
- it("classifies 401 as auth_failure", () => {
443
- expect(categorizeError(401, "Unauthorized")).toBe("auth_failure");
444
- expect(categorizeError(401, "api key invalid")).toBe("auth_failure");
445
- expect(categorizeError(401, "")).toBe("auth_failure");
446
- });
447
-
448
- it("classifies 403 with quota body as quota_exceeded", () => {
449
- expect(categorizeError(403, "plan limit reached")).toBe("quota_exceeded");
450
- expect(categorizeError(403, "quota exceeded for this month")).toBe("quota_exceeded");
451
- expect(categorizeError(403, "subscription required")).toBe("quota_exceeded");
452
- });
453
-
454
- it("classifies 403 without quota body as auth_failure", () => {
455
- expect(categorizeError(403, "Forbidden")).toBe("auth_failure");
456
- expect(categorizeError(403, "")).toBe("auth_failure");
457
- });
458
-
459
- it("classifies 402 as payment_error", () => {
460
- expect(categorizeError(402, "payment required")).toBe("payment_error");
461
- expect(categorizeError(402, "")).toBe("payment_error");
462
- });
463
-
464
- it("classifies 429 as rate_limited", () => {
465
- expect(categorizeError(429, "rate limit exceeded")).toBe("rate_limited");
466
- expect(categorizeError(429, "")).toBe("rate_limited");
467
- });
468
-
469
- it("classifies 529 as overloaded", () => {
470
- expect(categorizeError(529, "")).toBe("overloaded");
471
- expect(categorizeError(529, "overloaded")).toBe("overloaded");
472
- });
473
-
474
- it("classifies 503 with overload body as overloaded", () => {
475
- expect(categorizeError(503, "service overloaded, try again")).toBe("overloaded");
476
- expect(categorizeError(503, "over capacity")).toBe("overloaded");
477
- expect(categorizeError(503, "too many requests")).toBe("overloaded");
478
- });
479
-
480
- it("classifies 503 without overload body as server_error", () => {
481
- expect(categorizeError(503, "service unavailable")).toBe("server_error");
482
- expect(categorizeError(503, "")).toBe("server_error");
483
- });
484
-
485
- it("classifies 5xx as server_error", () => {
486
- expect(categorizeError(500, "internal server error")).toBe("server_error");
487
- expect(categorizeError(502, "bad gateway")).toBe("server_error");
488
- expect(categorizeError(504, "gateway timeout")).toBe("server_error");
489
- });
490
-
491
- it("classifies 413 with size body as config_error", () => {
492
- expect(categorizeError(413, "request too large")).toBe("config_error");
493
- expect(categorizeError(413, "payload too large")).toBe("config_error");
494
- });
495
-
496
- it("classifies 200 as null (not a provider error)", () => {
497
- expect(categorizeError(200, "ok")).toBeNull();
498
- });
499
-
500
- it("classifies bare 400 with no pattern match as null", () => {
501
- expect(categorizeError(400, "bad request")).toBeNull();
502
- });
503
-
504
- it("classifies 400 with billing body as config_error", () => {
505
- expect(categorizeError(400, "billing issue with account")).toBe("config_error");
506
- expect(categorizeError(400, "insufficient balance")).toBe("config_error");
507
- });
508
- });
509
- ```
510
-
511
- **Step 2: Run tests**
512
-
513
- ```bash
514
- cd /Users/vickyfu/Documents/blockrun-web/ClawRouter && bun test src/error-classification.test.ts
515
- ```
516
-
517
- Expected: all tests pass.
518
-
519
- **Step 3: Commit**
520
-
521
- ```bash
522
- git add src/error-classification.test.ts
523
- git commit -m "test: add error classification unit tests"
524
- ```
525
-
526
- ---
527
-
528
- ## Task 7: Version bump and full test run
529
-
530
- **Files:**
531
- - Modify: `package.json`
532
-
533
- **Step 1: Bump version**
534
-
535
- In `package.json`, change `"version": "0.12.57"` to `"version": "0.12.58"`.
536
-
537
- Add changelog entry comment in the commit message.
538
-
539
- **Step 2: Run full test suite**
540
-
541
- ```bash
542
- cd /Users/vickyfu/Documents/blockrun-web/ClawRouter && bun test 2>&1 | tail -20
543
- ```
544
-
545
- Expected: all existing tests still pass (new tests pass from Task 6).
546
-
547
- **Step 3: Build final**
548
-
549
- ```bash
550
- cd /Users/vickyfu/Documents/blockrun-web/ClawRouter && bun run build 2>&1
551
- ```
552
-
553
- **Step 4: Final commit**
554
-
555
- ```bash
556
- git add package.json
557
- git commit -m "chore: bump version to 0.12.58 (error classification)"
558
- ```
559
-
560
- ---
561
-
562
- ## Summary
563
-
564
- Total changes: all in `src/proxy.ts` + new `src/error-classification.test.ts` + `package.json`.
565
-
566
- Key behavioral changes:
567
- - **401** → `auth_failure` — logged with 🔑, fallback triggered, NOT added to `rateLimitedModels`
568
- - **403** → `quota_exceeded` or `auth_failure` — never contaminates rate-limit cooldown
569
- - **429** → `rate_limited` — existing 60s cooldown, unchanged
570
- - **529/503+overload** → `overloaded` — new 15s cooldown via `overloadedModels` map
571
- - **5xx** → `server_error` — immediate fallback, no cooldown
572
- - `prioritizeNonRateLimited()` updated to deprioritize both rate-limited AND overloaded models
573
- - `/stats` response gains `providerErrors` field with per-model breakdown
574
- - `categorizeError()` is exported for testing