@blockrun/cc 0.8.2 → 0.9.2
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/LICENSE +1 -1
- package/README.md +112 -7
- package/dist/commands/balance.js +8 -2
- package/dist/commands/models.js +8 -1
- package/dist/commands/start.d.ts +1 -0
- package/dist/commands/start.js +42 -18
- package/dist/commands/stats.d.ts +10 -0
- package/dist/commands/stats.js +94 -0
- package/dist/index.js +19 -1
- package/dist/proxy/fallback.d.ts +34 -0
- package/dist/proxy/fallback.js +115 -0
- package/dist/proxy/server.d.ts +1 -0
- package/dist/proxy/server.js +240 -41
- package/dist/router/index.d.ts +22 -0
- package/dist/router/index.js +281 -0
- package/dist/stats/tracker.d.ts +52 -0
- package/dist/stats/tracker.js +130 -0
- package/package.json +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ You're paying $200/month and still can't work.<br><br>
|
|
|
18
18
|
[](https://npmjs.com/package/@blockrun/cc)
|
|
19
19
|
[](https://github.com/BlockRunAI/brcc)
|
|
20
20
|
[](https://typescriptlang.org)
|
|
21
|
-
[](LICENSE)
|
|
22
22
|
|
|
23
23
|
[](https://x402.org)
|
|
24
24
|
[](https://base.org)
|
|
@@ -110,13 +110,43 @@ That's it. Claude Code opens with access to 40+ models, no rate limits.
|
|
|
110
110
|
### From the command line
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
|
-
brcc start # Default
|
|
114
|
-
brcc start --model
|
|
115
|
-
brcc start --model
|
|
116
|
-
brcc start --model
|
|
113
|
+
brcc start # Default: smart routing (blockrun/auto)
|
|
114
|
+
brcc start --model blockrun/eco # Cheapest capable model
|
|
115
|
+
brcc start --model blockrun/premium # Best quality
|
|
116
|
+
brcc start --model blockrun/free # Free tier only
|
|
117
|
+
brcc start --model deepseek/deepseek-chat # Direct model access
|
|
117
118
|
brcc start --model anthropic/claude-opus-4.6 # Most capable
|
|
118
119
|
```
|
|
119
120
|
|
|
121
|
+
### Smart Routing (Built-in)
|
|
122
|
+
|
|
123
|
+
brcc includes ClawRouter's 15-dimension classifier for automatic model selection:
|
|
124
|
+
|
|
125
|
+
| Profile | Strategy | Savings | Best For |
|
|
126
|
+
|---------|----------|---------|----------|
|
|
127
|
+
| `blockrun/auto` | Balanced (default) | 74-100% | General use |
|
|
128
|
+
| `blockrun/eco` | Cheapest possible | 95-100% | Maximum savings |
|
|
129
|
+
| `blockrun/premium` | Best quality | 0% | Mission-critical |
|
|
130
|
+
| `blockrun/free` | Free tier only | 100% | Zero cost |
|
|
131
|
+
|
|
132
|
+
**How it works:**
|
|
133
|
+
```
|
|
134
|
+
"What is 2+2?" → SIMPLE → gemini-flash ($0.0002)
|
|
135
|
+
"Write a React component" → MEDIUM → kimi-k2.5 ($0.002)
|
|
136
|
+
"Design a microservice..." → COMPLEX → gemini-3.1-pro ($0.007)
|
|
137
|
+
"Prove this theorem..." → REASONING → grok-4-fast ($0.0004)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**In-session switching:**
|
|
141
|
+
```
|
|
142
|
+
use auto # Switch to smart routing
|
|
143
|
+
use eco # Switch to cheapest
|
|
144
|
+
use premium # Switch to best quality
|
|
145
|
+
use free # Switch to free models
|
|
146
|
+
use sonnet # Direct Claude Sonnet
|
|
147
|
+
use deepseek # Direct DeepSeek
|
|
148
|
+
```
|
|
149
|
+
|
|
120
150
|
### Inside Claude Code
|
|
121
151
|
|
|
122
152
|
Use `/model` to switch between Sonnet, Opus, and Haiku. Each maps to the BlockRun model you've configured:
|
|
@@ -172,6 +202,7 @@ Paid Models
|
|
|
172
202
|
| `brcc start [--model <id>]` | Start proxy + launch Claude Code |
|
|
173
203
|
| `brcc models` | List all models with pricing |
|
|
174
204
|
| `brcc balance` | Check wallet USDC balance |
|
|
205
|
+
| `brcc stats` | View usage statistics and savings |
|
|
175
206
|
| `brcc config set <key> <value>` | Configure model mappings |
|
|
176
207
|
| `brcc config list` | View current settings |
|
|
177
208
|
|
|
@@ -190,11 +221,55 @@ Your wallet is saved to `~/.blockrun/` and shared with all BlockRun tools.
|
|
|
190
221
|
```bash
|
|
191
222
|
brcc start # Default model
|
|
192
223
|
brcc start --model nvidia/gpt-oss-120b # Free model
|
|
193
|
-
brcc start --model openai/gpt-5.4
|
|
224
|
+
brcc start --model openai/gpt-5.4 # Specific model
|
|
194
225
|
brcc start --no-launch # Proxy only mode
|
|
226
|
+
brcc start --no-fallback # Disable auto-fallback
|
|
195
227
|
brcc start -p 9000 # Custom port
|
|
196
228
|
```
|
|
197
229
|
|
|
230
|
+
### `brcc stats`
|
|
231
|
+
|
|
232
|
+
View your usage statistics and cost savings:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
$ brcc stats
|
|
236
|
+
|
|
237
|
+
📊 brcc Usage Statistics
|
|
238
|
+
|
|
239
|
+
───────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
Overview (7 days)
|
|
242
|
+
|
|
243
|
+
Requests: 1,234
|
|
244
|
+
Total Cost: $4.5672
|
|
245
|
+
Avg per Request: $0.003701
|
|
246
|
+
Input Tokens: 2,456,000
|
|
247
|
+
Output Tokens: 892,000
|
|
248
|
+
Fallbacks: 23 (1.9%)
|
|
249
|
+
|
|
250
|
+
By Model
|
|
251
|
+
|
|
252
|
+
anthropic/claude-sonnet-4.6
|
|
253
|
+
450 req · $2.1340 (46.7%) · 245ms avg
|
|
254
|
+
deepseek/deepseek-chat
|
|
255
|
+
620 req · $0.8901 (19.5%) · 180ms avg
|
|
256
|
+
↳ 12 fallback recoveries
|
|
257
|
+
nvidia/gpt-oss-120b
|
|
258
|
+
164 req · $0.0000 (0%) · 320ms avg
|
|
259
|
+
|
|
260
|
+
💰 Savings vs Claude Opus
|
|
261
|
+
|
|
262
|
+
Opus equivalent: $34.62
|
|
263
|
+
Your actual cost: $4.57
|
|
264
|
+
Saved: $30.05 (86.8%)
|
|
265
|
+
|
|
266
|
+
───────────────────────────────────────────────────────────
|
|
267
|
+
Run `brcc stats --clear` to reset statistics
|
|
268
|
+
|
|
269
|
+
$ brcc stats --clear # Reset all statistics
|
|
270
|
+
$ brcc stats --json # Output as JSON (for scripts)
|
|
271
|
+
```
|
|
272
|
+
|
|
198
273
|
### `brcc config`
|
|
199
274
|
|
|
200
275
|
```bash
|
|
@@ -207,6 +282,36 @@ brcc config list
|
|
|
207
282
|
|
|
208
283
|
---
|
|
209
284
|
|
|
285
|
+
## Automatic Fallback
|
|
286
|
+
|
|
287
|
+
When a model returns an error (429 rate limit, 500+ server error), brcc automatically retries with backup models. This ensures your work never stops.
|
|
288
|
+
|
|
289
|
+
**Default fallback chain:**
|
|
290
|
+
```
|
|
291
|
+
anthropic/claude-sonnet-4.6
|
|
292
|
+
↓ (if 429/500/502/503/504)
|
|
293
|
+
google/gemini-2.5-pro
|
|
294
|
+
↓
|
|
295
|
+
deepseek/deepseek-chat
|
|
296
|
+
↓
|
|
297
|
+
xai/grok-4-fast
|
|
298
|
+
↓
|
|
299
|
+
nvidia/gpt-oss-120b (free, always available)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**How it looks:**
|
|
303
|
+
```
|
|
304
|
+
[brcc] ⚠️ anthropic/claude-sonnet-4.6 returned 429, falling back to google/gemini-2.5-pro
|
|
305
|
+
[brcc] ↺ Fallback successful: using google/gemini-2.5-pro
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
To disable fallback:
|
|
309
|
+
```bash
|
|
310
|
+
brcc start --no-fallback
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
210
315
|
## How It Works
|
|
211
316
|
|
|
212
317
|
```
|
|
@@ -277,4 +382,4 @@ Yes. GPT-5, Gemini, DeepSeek, Grok, and 30+ others work through Claude Code via
|
|
|
277
382
|
|
|
278
383
|
## License
|
|
279
384
|
|
|
280
|
-
[Business Source License 1.1](LICENSE) — Free to use, modify, and deploy. Cannot be used to build a competing hosted service. Converts to MIT in
|
|
385
|
+
[Business Source License 1.1](LICENSE) — Free to use, modify, and deploy. Cannot be used to build a competing hosted service. Converts to MIT in 2036.
|
package/dist/commands/balance.js
CHANGED
|
@@ -27,8 +27,14 @@ export async function balanceCommand() {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
catch {
|
|
31
|
-
|
|
30
|
+
catch (err) {
|
|
31
|
+
const msg = err instanceof Error ? err.message : '';
|
|
32
|
+
if (msg.includes('ENOENT') || msg.includes('wallet') || msg.includes('key')) {
|
|
33
|
+
console.log(chalk.red('No wallet found. Run `brcc setup` first.'));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.log(chalk.red(`Error checking balance: ${msg || 'unknown error'}`));
|
|
37
|
+
}
|
|
32
38
|
process.exit(1);
|
|
33
39
|
}
|
|
34
40
|
}
|
package/dist/commands/models.js
CHANGED
|
@@ -37,6 +37,13 @@ export async function modelsCommand() {
|
|
|
37
37
|
console.log(`\n${chalk.dim(`${models.length} models available. Use:`)} ${chalk.bold('brcc start --model <model-id>')}`);
|
|
38
38
|
}
|
|
39
39
|
catch (err) {
|
|
40
|
-
|
|
40
|
+
const msg = err instanceof Error ? err.message : 'unknown error';
|
|
41
|
+
if (msg.includes('fetch') || msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
|
|
42
|
+
console.log(chalk.red(`Cannot reach BlockRun API at ${apiUrl}`));
|
|
43
|
+
console.log(chalk.dim('Check your internet connection or try again later.'));
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log(chalk.red(`Error: ${msg}`));
|
|
47
|
+
}
|
|
41
48
|
}
|
|
42
49
|
}
|
package/dist/commands/start.d.ts
CHANGED
package/dist/commands/start.js
CHANGED
|
@@ -7,6 +7,12 @@ import { loadConfig } from './config.js';
|
|
|
7
7
|
export async function startCommand(options) {
|
|
8
8
|
const chain = loadChain();
|
|
9
9
|
const apiUrl = API_URLS[chain];
|
|
10
|
+
const fallbackEnabled = options.fallback !== false; // Default true
|
|
11
|
+
const port = parseInt(options.port || String(DEFAULT_PROXY_PORT));
|
|
12
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
13
|
+
console.log(chalk.red(`Invalid port: ${options.port}. Must be 1-65535.`));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
10
16
|
if (chain === 'solana') {
|
|
11
17
|
const wallet = await getOrCreateSolanaWallet();
|
|
12
18
|
if (wallet.isNew) {
|
|
@@ -15,18 +21,25 @@ export async function startCommand(options) {
|
|
|
15
21
|
console.log(`\nSend USDC on Solana to this address, then run ${chalk.bold('brcc start')} again.\n`);
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
18
|
-
const port = parseInt(options.port || String(DEFAULT_PROXY_PORT));
|
|
19
24
|
const shouldLaunch = options.launch !== false;
|
|
20
25
|
const model = options.model;
|
|
21
26
|
console.log(chalk.bold('brcc — BlockRun Claude Code\n'));
|
|
22
|
-
console.log(`Chain:
|
|
23
|
-
console.log(`Wallet:
|
|
27
|
+
console.log(`Chain: ${chalk.magenta('solana')}`);
|
|
28
|
+
console.log(`Wallet: ${chalk.cyan(wallet.address)}`);
|
|
24
29
|
if (model)
|
|
25
|
-
console.log(`Model:
|
|
26
|
-
console.log(`
|
|
27
|
-
console.log(`
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
console.log(`Model: ${chalk.green(model)}`);
|
|
31
|
+
console.log(`Fallback: ${fallbackEnabled ? chalk.green('enabled') : chalk.yellow('disabled')}`);
|
|
32
|
+
console.log(`Proxy: ${chalk.cyan(`http://localhost:${port}`)}`);
|
|
33
|
+
console.log(`Backend: ${chalk.dim(apiUrl)}\n`);
|
|
34
|
+
const server = createProxy({
|
|
35
|
+
port,
|
|
36
|
+
apiUrl,
|
|
37
|
+
chain: 'solana',
|
|
38
|
+
modelOverride: model,
|
|
39
|
+
debug: options.debug,
|
|
40
|
+
fallbackEnabled,
|
|
41
|
+
});
|
|
42
|
+
launchServer(server, port, shouldLaunch, model, options.debug);
|
|
30
43
|
}
|
|
31
44
|
else {
|
|
32
45
|
const wallet = getOrCreateWallet();
|
|
@@ -36,23 +49,34 @@ export async function startCommand(options) {
|
|
|
36
49
|
console.log(`\nSend USDC on Base to this address, then run ${chalk.bold('brcc start')} again.\n`);
|
|
37
50
|
return;
|
|
38
51
|
}
|
|
39
|
-
const port = parseInt(options.port || String(DEFAULT_PROXY_PORT));
|
|
40
52
|
const shouldLaunch = options.launch !== false;
|
|
41
53
|
const model = options.model;
|
|
42
54
|
console.log(chalk.bold('brcc — BlockRun Claude Code\n'));
|
|
43
|
-
console.log(`Chain:
|
|
44
|
-
console.log(`Wallet:
|
|
55
|
+
console.log(`Chain: ${chalk.magenta('base')}`);
|
|
56
|
+
console.log(`Wallet: ${chalk.cyan(wallet.address)}`);
|
|
45
57
|
if (model)
|
|
46
|
-
console.log(`Model:
|
|
47
|
-
console.log(`
|
|
48
|
-
console.log(`
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
console.log(`Model: ${chalk.green(model)}`);
|
|
59
|
+
console.log(`Fallback: ${fallbackEnabled ? chalk.green('enabled') : chalk.yellow('disabled')}`);
|
|
60
|
+
console.log(`Proxy: ${chalk.cyan(`http://localhost:${port}`)}`);
|
|
61
|
+
console.log(`Backend: ${chalk.dim(apiUrl)}\n`);
|
|
62
|
+
const server = createProxy({
|
|
63
|
+
port,
|
|
64
|
+
apiUrl,
|
|
65
|
+
chain: 'base',
|
|
66
|
+
modelOverride: model,
|
|
67
|
+
debug: options.debug,
|
|
68
|
+
fallbackEnabled,
|
|
69
|
+
});
|
|
70
|
+
launchServer(server, port, shouldLaunch, model, options.debug);
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
|
-
function launchServer(server, port, shouldLaunch, model) {
|
|
73
|
+
function launchServer(server, port, shouldLaunch, model, debug) {
|
|
54
74
|
server.listen(port, () => {
|
|
55
|
-
console.log(chalk.green(
|
|
75
|
+
console.log(chalk.green(`✓ Proxy running on port ${port}`));
|
|
76
|
+
console.log(chalk.dim(` Usage tracking: ~/.blockrun/brcc-stats.json`));
|
|
77
|
+
if (debug)
|
|
78
|
+
console.log(chalk.dim(` Debug log: ~/.blockrun/brcc-debug.log`));
|
|
79
|
+
console.log(chalk.dim(` Run 'brcc stats' to view statistics\n`));
|
|
56
80
|
if (shouldLaunch) {
|
|
57
81
|
console.log('Starting Claude Code...\n');
|
|
58
82
|
const cleanEnv = { ...process.env };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brcc stats command
|
|
3
|
+
* Display usage statistics and cost savings
|
|
4
|
+
*/
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { clearStats, getStatsSummary } from '../stats/tracker.js';
|
|
7
|
+
export function statsCommand(options) {
|
|
8
|
+
if (options.clear) {
|
|
9
|
+
clearStats();
|
|
10
|
+
console.log(chalk.green('✓ Statistics cleared'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const { stats, opusCost, saved, savedPct, avgCostPerRequest, period } = getStatsSummary();
|
|
14
|
+
// JSON output for programmatic access
|
|
15
|
+
if (options.json) {
|
|
16
|
+
console.log(JSON.stringify({
|
|
17
|
+
...stats,
|
|
18
|
+
computed: {
|
|
19
|
+
opusCost,
|
|
20
|
+
saved,
|
|
21
|
+
savedPct,
|
|
22
|
+
avgCostPerRequest,
|
|
23
|
+
period,
|
|
24
|
+
},
|
|
25
|
+
}, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Pretty output
|
|
29
|
+
console.log(chalk.bold('\n📊 brcc Usage Statistics\n'));
|
|
30
|
+
console.log('─'.repeat(55));
|
|
31
|
+
if (stats.totalRequests === 0) {
|
|
32
|
+
console.log(chalk.gray('\n No requests recorded yet. Start using brcc!\n'));
|
|
33
|
+
console.log('─'.repeat(55) + '\n');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Overview
|
|
37
|
+
console.log(chalk.bold('\n Overview') + chalk.gray(` (${period})\n`));
|
|
38
|
+
console.log(` Requests: ${chalk.cyan(stats.totalRequests.toLocaleString())}`);
|
|
39
|
+
console.log(` Total Cost: ${chalk.green('$' + stats.totalCostUsd.toFixed(4))}`);
|
|
40
|
+
console.log(` Avg per Request:${chalk.gray(' $' + avgCostPerRequest.toFixed(6))}`);
|
|
41
|
+
console.log(` Input Tokens: ${stats.totalInputTokens.toLocaleString()}`);
|
|
42
|
+
console.log(` Output Tokens: ${stats.totalOutputTokens.toLocaleString()}`);
|
|
43
|
+
if (stats.totalFallbacks > 0) {
|
|
44
|
+
const fallbackPct = ((stats.totalFallbacks / stats.totalRequests) *
|
|
45
|
+
100).toFixed(1);
|
|
46
|
+
console.log(` Fallbacks: ${chalk.yellow(stats.totalFallbacks.toString())} (${fallbackPct}%)`);
|
|
47
|
+
}
|
|
48
|
+
// Per-model breakdown
|
|
49
|
+
const modelEntries = Object.entries(stats.byModel);
|
|
50
|
+
if (modelEntries.length > 0) {
|
|
51
|
+
console.log(chalk.bold('\n By Model\n'));
|
|
52
|
+
// Sort by cost (descending)
|
|
53
|
+
const sorted = modelEntries.sort((a, b) => b[1].costUsd - a[1].costUsd);
|
|
54
|
+
for (const [model, data] of sorted) {
|
|
55
|
+
const pct = stats.totalCostUsd > 0
|
|
56
|
+
? ((data.costUsd / stats.totalCostUsd) * 100).toFixed(1)
|
|
57
|
+
: '0';
|
|
58
|
+
const avgLatency = Math.round(data.avgLatencyMs);
|
|
59
|
+
// Shorten model name if too long
|
|
60
|
+
const displayModel = model.length > 35 ? model.slice(0, 32) + '...' : model;
|
|
61
|
+
console.log(` ${chalk.cyan(displayModel)}`);
|
|
62
|
+
console.log(chalk.gray(` ${data.requests} req · $${data.costUsd.toFixed(4)} (${pct}%) · ${avgLatency}ms avg`));
|
|
63
|
+
if (data.fallbackCount > 0) {
|
|
64
|
+
console.log(chalk.yellow(` ↳ ${data.fallbackCount} fallback recoveries`));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Savings comparison
|
|
69
|
+
console.log(chalk.bold('\n 💰 Savings vs Claude Opus\n'));
|
|
70
|
+
if (opusCost > 0) {
|
|
71
|
+
console.log(` Opus equivalent: ${chalk.gray('$' + opusCost.toFixed(2))}`);
|
|
72
|
+
console.log(` Your actual cost:${chalk.green(' $' + stats.totalCostUsd.toFixed(2))}`);
|
|
73
|
+
console.log(` ${chalk.green.bold(`Saved: $${saved.toFixed(2)} (${savedPct.toFixed(1)}%)`)}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.log(chalk.gray(' Not enough data to calculate savings'));
|
|
77
|
+
}
|
|
78
|
+
// Recent activity (last 5 requests)
|
|
79
|
+
if (stats.history.length > 0) {
|
|
80
|
+
console.log(chalk.bold('\n Recent Activity\n'));
|
|
81
|
+
const recent = stats.history.slice(-5).reverse();
|
|
82
|
+
for (const record of recent) {
|
|
83
|
+
const time = new Date(record.timestamp).toLocaleTimeString();
|
|
84
|
+
const model = record.model.split('/').pop() || record.model;
|
|
85
|
+
const cost = '$' + record.costUsd.toFixed(4);
|
|
86
|
+
const fallbackMark = record.fallback ? chalk.yellow(' ↺') : '';
|
|
87
|
+
console.log(chalk.gray(` ${time}`) +
|
|
88
|
+
` ${model}${fallbackMark} ` +
|
|
89
|
+
chalk.green(cost));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.log('\n' + '─'.repeat(55));
|
|
93
|
+
console.log(chalk.gray(' Run `brcc stats --clear` to reset statistics\n'));
|
|
94
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -5,12 +5,23 @@ import { startCommand } from './commands/start.js';
|
|
|
5
5
|
import { balanceCommand } from './commands/balance.js';
|
|
6
6
|
import { modelsCommand } from './commands/models.js';
|
|
7
7
|
import { configCommand } from './commands/config.js';
|
|
8
|
+
import { statsCommand } from './commands/stats.js';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
let version = '0.9.0';
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
|
|
16
|
+
version = pkg.version || version;
|
|
17
|
+
}
|
|
18
|
+
catch { /* use default */ }
|
|
8
19
|
const program = new Command();
|
|
9
20
|
program
|
|
10
21
|
.name('brcc')
|
|
11
22
|
.description('BlockRun Claude Code — run Claude Code with any model, pay with USDC.\n\n' +
|
|
12
23
|
'Use /model inside Claude Code to switch between models on the fly.')
|
|
13
|
-
.version(
|
|
24
|
+
.version(version);
|
|
14
25
|
program
|
|
15
26
|
.command('setup [chain]')
|
|
16
27
|
.description('Create a new wallet for payments (base or solana)')
|
|
@@ -21,6 +32,7 @@ program
|
|
|
21
32
|
.option('-p, --port <port>', 'Proxy port', '8402')
|
|
22
33
|
.option('-m, --model <model>', 'Default model (e.g. openai/gpt-5.4, anthropic/claude-sonnet-4.6)')
|
|
23
34
|
.option('--no-launch', 'Start proxy only, do not launch Claude Code')
|
|
35
|
+
.option('--no-fallback', 'Disable automatic fallback to backup models')
|
|
24
36
|
.option('--debug', 'Enable debug logging')
|
|
25
37
|
.action(startCommand);
|
|
26
38
|
program
|
|
@@ -36,4 +48,10 @@ program
|
|
|
36
48
|
.description('Manage brcc config (set, get, unset, list)\n' +
|
|
37
49
|
'Keys: default-model, sonnet-model, opus-model, haiku-model, smart-routing')
|
|
38
50
|
.action(configCommand);
|
|
51
|
+
program
|
|
52
|
+
.command('stats')
|
|
53
|
+
.description('Show usage statistics and cost savings')
|
|
54
|
+
.option('--clear', 'Clear all statistics')
|
|
55
|
+
.option('--json', 'Output in JSON format')
|
|
56
|
+
.action(statsCommand);
|
|
39
57
|
program.parse();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback chain for brcc
|
|
3
|
+
* Automatically switches to backup models when primary fails (429, 5xx, etc.)
|
|
4
|
+
*/
|
|
5
|
+
export interface FallbackConfig {
|
|
6
|
+
/** Models to try in order of priority */
|
|
7
|
+
chain: string[];
|
|
8
|
+
/** HTTP status codes that trigger fallback */
|
|
9
|
+
retryOn: number[];
|
|
10
|
+
/** Maximum retries across all models */
|
|
11
|
+
maxRetries: number;
|
|
12
|
+
/** Delay between retries in ms */
|
|
13
|
+
retryDelayMs: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_FALLBACK_CONFIG: FallbackConfig;
|
|
16
|
+
export interface FallbackResult {
|
|
17
|
+
response: Response;
|
|
18
|
+
modelUsed: string;
|
|
19
|
+
fallbackUsed: boolean;
|
|
20
|
+
attemptsCount: number;
|
|
21
|
+
failedModels: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fetch with automatic fallback to backup models
|
|
25
|
+
*/
|
|
26
|
+
export declare function fetchWithFallback(url: string, init: RequestInit, originalBody: string, config?: FallbackConfig, onFallback?: (model: string, statusCode: number, nextModel: string) => void): Promise<FallbackResult>;
|
|
27
|
+
/**
|
|
28
|
+
* Get the current model from fallback chain based on parsed request
|
|
29
|
+
*/
|
|
30
|
+
export declare function getCurrentModelFromChain(requestedModel: string | undefined, config?: FallbackConfig): string;
|
|
31
|
+
/**
|
|
32
|
+
* Build fallback chain starting from a specific model
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildFallbackChain(startModel: string, config?: FallbackConfig): string[];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback chain for brcc
|
|
3
|
+
* Automatically switches to backup models when primary fails (429, 5xx, etc.)
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_FALLBACK_CONFIG = {
|
|
6
|
+
chain: [
|
|
7
|
+
'blockrun/auto', // Smart routing (default)
|
|
8
|
+
'blockrun/eco', // Cheapest capable model
|
|
9
|
+
'deepseek/deepseek-chat', // Direct fallback
|
|
10
|
+
'nvidia/gpt-oss-120b', // Free model as ultimate fallback
|
|
11
|
+
],
|
|
12
|
+
retryOn: [429, 500, 502, 503, 504, 529],
|
|
13
|
+
maxRetries: 5,
|
|
14
|
+
retryDelayMs: 1000,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Sleep helper
|
|
18
|
+
*/
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Replace model in request body
|
|
24
|
+
*/
|
|
25
|
+
function replaceModelInBody(body, newModel) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(body);
|
|
28
|
+
parsed.model = newModel;
|
|
29
|
+
return JSON.stringify(parsed);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return body;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Fetch with automatic fallback to backup models
|
|
37
|
+
*/
|
|
38
|
+
export async function fetchWithFallback(url, init, originalBody, config = DEFAULT_FALLBACK_CONFIG, onFallback) {
|
|
39
|
+
const failedModels = [];
|
|
40
|
+
let attempts = 0;
|
|
41
|
+
for (let i = 0; i < config.chain.length && attempts < config.maxRetries; i++) {
|
|
42
|
+
const model = config.chain[i];
|
|
43
|
+
const body = replaceModelInBody(originalBody, model);
|
|
44
|
+
try {
|
|
45
|
+
attempts++;
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
...init,
|
|
48
|
+
body,
|
|
49
|
+
});
|
|
50
|
+
// Success or non-retryable error
|
|
51
|
+
if (!config.retryOn.includes(response.status)) {
|
|
52
|
+
return {
|
|
53
|
+
response,
|
|
54
|
+
modelUsed: model,
|
|
55
|
+
fallbackUsed: i > 0,
|
|
56
|
+
attemptsCount: attempts,
|
|
57
|
+
failedModels,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Retryable error - log and try next
|
|
61
|
+
failedModels.push(model);
|
|
62
|
+
const nextModel = config.chain[i + 1];
|
|
63
|
+
if (nextModel && onFallback) {
|
|
64
|
+
onFallback(model, response.status, nextModel);
|
|
65
|
+
}
|
|
66
|
+
// Wait before trying next model (with exponential backoff for same model retries)
|
|
67
|
+
if (i < config.chain.length - 1) {
|
|
68
|
+
await sleep(config.retryDelayMs);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// Network error - try next model
|
|
73
|
+
failedModels.push(model);
|
|
74
|
+
const nextModel = config.chain[i + 1];
|
|
75
|
+
if (nextModel && onFallback) {
|
|
76
|
+
const errMsg = err instanceof Error ? err.message : 'Network error';
|
|
77
|
+
onFallback(model, 0, nextModel);
|
|
78
|
+
console.error(`[fallback] ${model} network error: ${errMsg}`);
|
|
79
|
+
}
|
|
80
|
+
if (i < config.chain.length - 1) {
|
|
81
|
+
await sleep(config.retryDelayMs);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// All models failed - throw error
|
|
86
|
+
throw new Error(`All models in fallback chain failed: ${failedModels.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get the current model from fallback chain based on parsed request
|
|
90
|
+
*/
|
|
91
|
+
export function getCurrentModelFromChain(requestedModel, config = DEFAULT_FALLBACK_CONFIG) {
|
|
92
|
+
// If model is explicitly set and in chain, start from there
|
|
93
|
+
if (requestedModel) {
|
|
94
|
+
const index = config.chain.indexOf(requestedModel);
|
|
95
|
+
if (index >= 0) {
|
|
96
|
+
return requestedModel;
|
|
97
|
+
}
|
|
98
|
+
// Model not in chain, use as-is (user specified custom model)
|
|
99
|
+
return requestedModel;
|
|
100
|
+
}
|
|
101
|
+
// Default to first model in chain
|
|
102
|
+
return config.chain[0];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build fallback chain starting from a specific model
|
|
106
|
+
*/
|
|
107
|
+
export function buildFallbackChain(startModel, config = DEFAULT_FALLBACK_CONFIG) {
|
|
108
|
+
const index = config.chain.indexOf(startModel);
|
|
109
|
+
if (index >= 0) {
|
|
110
|
+
// Start from this model and include all after it
|
|
111
|
+
return config.chain.slice(index);
|
|
112
|
+
}
|
|
113
|
+
// Model not in default chain - prepend it
|
|
114
|
+
return [startModel, ...config.chain];
|
|
115
|
+
}
|
package/dist/proxy/server.d.ts
CHANGED