@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,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
|