@blockrun/clawrouter 0.3.41 → 0.4.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/README.md +32 -33
- package/dist/index.d.ts +10 -1
- package/dist/index.js +311 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ One wallet, 30+ models, zero API keys.
|
|
|
12
12
|
[](https://typescriptlang.org)
|
|
13
13
|
[](https://nodejs.org)
|
|
14
14
|
|
|
15
|
-
[Docs](https://blockrun.ai/docs) · [Models](https://blockrun.ai/models) · [Telegram](https://t.me/blockrunAI) · [X](https://x.com/BlockRunAI)
|
|
15
|
+
[Docs](https://blockrun.ai/docs) · [Models](https://blockrun.ai/models) · [Configuration](docs/configuration.md) · [Architecture](docs/architecture.md) · [Telegram](https://t.me/blockrunAI) · [X](https://x.com/BlockRunAI)
|
|
16
16
|
|
|
17
17
|
</div>
|
|
18
18
|
|
|
@@ -283,35 +283,31 @@ If you explicitly want to use a different wallet:
|
|
|
283
283
|
|
|
284
284
|
Routing is **client-side** — open source and inspectable.
|
|
285
285
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
```
|
|
289
|
-
src/
|
|
290
|
-
├── index.ts # Plugin entry point
|
|
291
|
-
├── provider.ts # OpenClaw provider registration
|
|
292
|
-
├── proxy.ts # Local HTTP proxy + x402 payment
|
|
293
|
-
├── models.ts # 30+ model definitions with pricing
|
|
294
|
-
├── auth.ts # Wallet key resolution
|
|
295
|
-
├── logger.ts # JSON usage logging
|
|
296
|
-
├── dedup.ts # Response deduplication (prevents double-charge)
|
|
297
|
-
├── payment-cache.ts # Pre-auth optimization (skips 402 round trip)
|
|
298
|
-
├── x402.ts # EIP-712 USDC payment signing
|
|
299
|
-
└── router/
|
|
300
|
-
├── index.ts # route() entry point
|
|
301
|
-
├── rules.ts # 14-dimension weighted scoring
|
|
302
|
-
├── selector.ts # Tier → model selection
|
|
303
|
-
├── config.ts # Default routing config
|
|
304
|
-
└── types.ts # TypeScript types
|
|
305
|
-
```
|
|
286
|
+
**Deep dive:** [docs/architecture.md](docs/architecture.md) — request flow, payment system, optimizations
|
|
306
287
|
|
|
307
288
|
---
|
|
308
289
|
|
|
309
290
|
## Configuration
|
|
310
291
|
|
|
311
|
-
|
|
292
|
+
For basic usage, no configuration is needed. For advanced options:
|
|
293
|
+
|
|
294
|
+
| Setting | Default | Description |
|
|
295
|
+
| --------------------- | -------- | ---------------------------- |
|
|
296
|
+
| `BLOCKRUN_PROXY_PORT` | `8402` | Proxy port (env var) |
|
|
297
|
+
| `BLOCKRUN_WALLET_KEY` | auto | Wallet private key (env var) |
|
|
298
|
+
| `routing.tiers` | see docs | Override tier→model mappings |
|
|
299
|
+
| `routing.scoring` | see docs | Custom keyword weights |
|
|
300
|
+
|
|
301
|
+
**Quick example:**
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
# Use different port
|
|
305
|
+
export BLOCKRUN_PROXY_PORT=8403
|
|
306
|
+
openclaw gateway restart
|
|
307
|
+
```
|
|
312
308
|
|
|
313
309
|
```yaml
|
|
314
|
-
# openclaw.yaml
|
|
310
|
+
# openclaw.yaml — override models
|
|
315
311
|
plugins:
|
|
316
312
|
- id: "@blockrun/clawrouter"
|
|
317
313
|
config:
|
|
@@ -319,18 +315,9 @@ plugins:
|
|
|
319
315
|
tiers:
|
|
320
316
|
COMPLEX:
|
|
321
317
|
primary: "openai/gpt-4o"
|
|
322
|
-
SIMPLE:
|
|
323
|
-
primary: "google/gemini-2.5-flash"
|
|
324
318
|
```
|
|
325
319
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
```yaml
|
|
329
|
-
routing:
|
|
330
|
-
scoring:
|
|
331
|
-
reasoningKeywords: ["proof", "theorem", "formal verification"]
|
|
332
|
-
codeKeywords: ["function", "class", "async", "await"]
|
|
333
|
-
```
|
|
320
|
+
**Full reference:** [docs/configuration.md](docs/configuration.md)
|
|
334
321
|
|
|
335
322
|
---
|
|
336
323
|
|
|
@@ -478,6 +465,18 @@ See [`openclaw.security.json`](openclaw.security.json) for detailed security doc
|
|
|
478
465
|
|
|
479
466
|
### Port 8402 already in use
|
|
480
467
|
|
|
468
|
+
As of v0.4.1, ClawRouter automatically detects and reuses an existing proxy on the configured port instead of failing with `EADDRINUSE`. You should no longer see this error.
|
|
469
|
+
|
|
470
|
+
If you need to use a different port:
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
# Set custom port via environment variable
|
|
474
|
+
export BLOCKRUN_PROXY_PORT=8403
|
|
475
|
+
openclaw gateway restart
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
To manually check/kill the process:
|
|
479
|
+
|
|
481
480
|
```bash
|
|
482
481
|
lsof -i :8402
|
|
483
482
|
# Kill the process or restart OpenClaw
|
package/dist/index.d.ts
CHANGED
|
@@ -363,6 +363,10 @@ declare class BalanceMonitor {
|
|
|
363
363
|
* - Usage logging: log every request as JSON line to ~/.openclaw/blockrun/logs/
|
|
364
364
|
*/
|
|
365
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Get the proxy port from environment variable or default.
|
|
368
|
+
*/
|
|
369
|
+
declare function getProxyPort(): number;
|
|
366
370
|
/** Callback info for low balance warning */
|
|
367
371
|
type LowBalanceInfo = {
|
|
368
372
|
balanceUSD: string;
|
|
@@ -382,6 +386,8 @@ type ProxyOptions = {
|
|
|
382
386
|
routingConfig?: Partial<RoutingConfig>;
|
|
383
387
|
/** Request timeout in ms (default: 180000 = 3 minutes). Covers on-chain tx + LLM response. */
|
|
384
388
|
requestTimeoutMs?: number;
|
|
389
|
+
/** Skip balance checks (for testing only). Default: false */
|
|
390
|
+
skipBalanceCheck?: boolean;
|
|
385
391
|
onReady?: (port: number) => void;
|
|
386
392
|
onError?: (error: Error) => void;
|
|
387
393
|
onPayment?: (info: {
|
|
@@ -405,6 +411,9 @@ type ProxyHandle = {
|
|
|
405
411
|
/**
|
|
406
412
|
* Start the local x402 proxy server.
|
|
407
413
|
*
|
|
414
|
+
* If a proxy is already running on the target port, reuses it instead of failing.
|
|
415
|
+
* Port can be configured via BLOCKRUN_PROXY_PORT environment variable.
|
|
416
|
+
*
|
|
408
417
|
* Returns a handle with the assigned port, base URL, and a close function.
|
|
409
418
|
*/
|
|
410
419
|
declare function startProxy(options: ProxyOptions): Promise<ProxyHandle>;
|
|
@@ -686,4 +695,4 @@ declare function isRetryable(errorOrResponse: Error | Response, config?: Partial
|
|
|
686
695
|
|
|
687
696
|
declare const plugin: OpenClawPluginDefinition;
|
|
688
697
|
|
|
689
|
-
export { BALANCE_THRESHOLDS, BLOCKRUN_MODELS, type BalanceInfo, BalanceMonitor, type CachedPaymentParams, type CachedResponse, DEFAULT_RETRY_CONFIG, DEFAULT_ROUTING_CONFIG, EmptyWalletError, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, OPENCLAW_MODELS, PaymentCache, type PaymentFetchResult, type PreAuthParams, type ProxyHandle, type ProxyOptions, RequestDeduplicator, type RetryConfig, type RoutingConfig, type RoutingDecision, RpcError, type SufficiencyResult, type Tier, type UsageEntry, blockrunProvider, buildProviderModels, createPaymentFetch, plugin as default, fetchWithRetry, isBalanceError, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, logUsage, route, startProxy };
|
|
698
|
+
export { BALANCE_THRESHOLDS, BLOCKRUN_MODELS, type BalanceInfo, BalanceMonitor, type CachedPaymentParams, type CachedResponse, DEFAULT_RETRY_CONFIG, DEFAULT_ROUTING_CONFIG, EmptyWalletError, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, OPENCLAW_MODELS, PaymentCache, type PaymentFetchResult, type PreAuthParams, type ProxyHandle, type ProxyOptions, RequestDeduplicator, type RetryConfig, type RoutingConfig, type RoutingDecision, RpcError, type SufficiencyResult, type Tier, type UsageEntry, blockrunProvider, buildProviderModels, createPaymentFetch, plugin as default, fetchWithRetry, getProxyPort, isBalanceError, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, logUsage, route, startProxy };
|
package/dist/index.js
CHANGED
|
@@ -712,6 +712,10 @@ function selectModel(tier, confidence, method, reasoning, tierConfigs, modelPric
|
|
|
712
712
|
savings
|
|
713
713
|
};
|
|
714
714
|
}
|
|
715
|
+
function getFallbackChain(tier, tierConfigs) {
|
|
716
|
+
const config = tierConfigs[tier];
|
|
717
|
+
return [config.primary, ...config.fallback];
|
|
718
|
+
}
|
|
715
719
|
|
|
716
720
|
// src/router/config.ts
|
|
717
721
|
var DEFAULT_ROUTING_CONFIG = {
|
|
@@ -763,10 +767,13 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
763
767
|
"\u0444\u0443\u043D\u043A\u0446\u0438\u044F",
|
|
764
768
|
"\u043A\u043B\u0430\u0441\u0441",
|
|
765
769
|
"\u0438\u043C\u043F\u043E\u0440\u0442",
|
|
770
|
+
"\u043E\u043F\u0440\u0435\u0434\u0435\u043B",
|
|
766
771
|
"\u0437\u0430\u043F\u0440\u043E\u0441",
|
|
767
772
|
"\u0430\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u043D\u044B\u0439",
|
|
773
|
+
"\u043E\u0436\u0438\u0434\u0430\u0442\u044C",
|
|
768
774
|
"\u043A\u043E\u043D\u0441\u0442\u0430\u043D\u0442\u0430",
|
|
769
|
-
"\u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u0430\u044F"
|
|
775
|
+
"\u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u0430\u044F",
|
|
776
|
+
"\u0432\u0435\u0440\u043D\u0443\u0442\u044C"
|
|
770
777
|
],
|
|
771
778
|
reasoningKeywords: [
|
|
772
779
|
// English
|
|
@@ -796,10 +803,15 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
796
803
|
"\u8AD6\u7406\u7684",
|
|
797
804
|
// Russian
|
|
798
805
|
"\u0434\u043E\u043A\u0430\u0437\u0430\u0442\u044C",
|
|
806
|
+
"\u0434\u043E\u043A\u0430\u0436\u0438",
|
|
807
|
+
"\u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432",
|
|
799
808
|
"\u0442\u0435\u043E\u0440\u0435\u043C\u0430",
|
|
800
809
|
"\u0432\u044B\u0432\u0435\u0441\u0442\u0438",
|
|
801
810
|
"\u0448\u0430\u0433 \u0437\u0430 \u0448\u0430\u0433\u043E\u043C",
|
|
811
|
+
"\u043F\u043E\u0448\u0430\u0433\u043E\u0432\u043E",
|
|
812
|
+
"\u043F\u043E\u044D\u0442\u0430\u043F\u043D\u043E",
|
|
802
813
|
"\u0446\u0435\u043F\u043E\u0447\u043A\u0430 \u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043D\u0438\u0439",
|
|
814
|
+
"\u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043D\u0438",
|
|
803
815
|
"\u0444\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u043E",
|
|
804
816
|
"\u043C\u0430\u0442\u0435\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438",
|
|
805
817
|
"\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438"
|
|
@@ -837,11 +849,14 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
837
849
|
"\u0447\u0442\u043E \u0442\u0430\u043A\u043E\u0435",
|
|
838
850
|
"\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435",
|
|
839
851
|
"\u043F\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438",
|
|
852
|
+
"\u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0438",
|
|
840
853
|
"\u043F\u0440\u0438\u0432\u0435\u0442",
|
|
841
854
|
"\u0434\u0430 \u0438\u043B\u0438 \u043D\u0435\u0442",
|
|
842
855
|
"\u0441\u0442\u043E\u043B\u0438\u0446\u0430",
|
|
856
|
+
"\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043B\u0435\u0442",
|
|
843
857
|
"\u043A\u0442\u043E \u0442\u0430\u043A\u043E\u0439",
|
|
844
|
-
"\u043A\u043E\u0433\u0434\u0430"
|
|
858
|
+
"\u043A\u043E\u0433\u0434\u0430",
|
|
859
|
+
"\u043E\u0431\u044A\u044F\u0441\u043D\u0438"
|
|
845
860
|
],
|
|
846
861
|
technicalKeywords: [
|
|
847
862
|
// English
|
|
@@ -871,6 +886,8 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
871
886
|
// Russian
|
|
872
887
|
"\u0430\u043B\u0433\u043E\u0440\u0438\u0442\u043C",
|
|
873
888
|
"\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
|
|
889
|
+
"\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0430\u0446\u0438",
|
|
890
|
+
"\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u0443\u0439",
|
|
874
891
|
"\u0430\u0440\u0445\u0438\u0442\u0435\u043A\u0442\u0443\u0440\u0430",
|
|
875
892
|
"\u0440\u0430\u0441\u043F\u0440\u0435\u0434\u0435\u043B\u0451\u043D\u043D\u044B\u0439",
|
|
876
893
|
"\u043C\u0438\u043A\u0440\u043E\u0441\u0435\u0440\u0432\u0438\u0441",
|
|
@@ -903,11 +920,14 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
903
920
|
"\u60F3\u50CF",
|
|
904
921
|
// Russian
|
|
905
922
|
"\u0438\u0441\u0442\u043E\u0440\u0438\u044F",
|
|
923
|
+
"\u0440\u0430\u0441\u0441\u043A\u0430\u0437",
|
|
906
924
|
"\u0441\u0442\u0438\u0445\u043E\u0442\u0432\u043E\u0440\u0435\u043D\u0438\u0435",
|
|
907
925
|
"\u0441\u043E\u0447\u0438\u043D\u0438\u0442\u044C",
|
|
926
|
+
"\u0441\u043E\u0447\u0438\u043D\u0438",
|
|
908
927
|
"\u043C\u043E\u0437\u0433\u043E\u0432\u043E\u0439 \u0448\u0442\u0443\u0440\u043C",
|
|
909
928
|
"\u0442\u0432\u043E\u0440\u0447\u0435\u0441\u043A\u0438\u0439",
|
|
910
929
|
"\u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C",
|
|
930
|
+
"\u043F\u0440\u0438\u0434\u0443\u043C\u0430\u0439",
|
|
911
931
|
"\u043D\u0430\u043F\u0438\u0448\u0438"
|
|
912
932
|
],
|
|
913
933
|
// New dimension keyword lists (multilingual)
|
|
@@ -944,13 +964,21 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
944
964
|
"\u8A2D\u5B9A",
|
|
945
965
|
// Russian
|
|
946
966
|
"\u043F\u043E\u0441\u0442\u0440\u043E\u0438\u0442\u044C",
|
|
967
|
+
"\u043F\u043E\u0441\u0442\u0440\u043E\u0439",
|
|
947
968
|
"\u0441\u043E\u0437\u0434\u0430\u0442\u044C",
|
|
969
|
+
"\u0441\u043E\u0437\u0434\u0430\u0439",
|
|
948
970
|
"\u0440\u0435\u0430\u043B\u0438\u0437\u043E\u0432\u0430\u0442\u044C",
|
|
971
|
+
"\u0440\u0435\u0430\u043B\u0438\u0437\u0443\u0439",
|
|
949
972
|
"\u0441\u043F\u0440\u043E\u0435\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
|
|
950
973
|
"\u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C",
|
|
974
|
+
"\u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0430\u0439",
|
|
975
|
+
"\u0441\u043A\u043E\u043D\u0441\u0442\u0440\u0443\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
|
|
951
976
|
"\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
|
|
977
|
+
"\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439",
|
|
952
978
|
"\u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C",
|
|
953
|
-
"\
|
|
979
|
+
"\u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0438",
|
|
980
|
+
"\u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C",
|
|
981
|
+
"\u043D\u0430\u0441\u0442\u0440\u043E\u0439"
|
|
954
982
|
],
|
|
955
983
|
constraintIndicators: [
|
|
956
984
|
// English
|
|
@@ -981,7 +1009,9 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
981
1009
|
"\u4E88\u7B97",
|
|
982
1010
|
// Russian
|
|
983
1011
|
"\u043D\u0435 \u0431\u043E\u043B\u0435\u0435",
|
|
1012
|
+
"\u043D\u0435 \u043C\u0435\u043D\u0435\u0435",
|
|
984
1013
|
"\u043A\u0430\u043A \u043C\u0438\u043D\u0438\u043C\u0443\u043C",
|
|
1014
|
+
"\u0432 \u043F\u0440\u0435\u0434\u0435\u043B\u0430\u0445",
|
|
985
1015
|
"\u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C",
|
|
986
1016
|
"\u043C\u0438\u043D\u0438\u043C\u0443\u043C",
|
|
987
1017
|
"\u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435",
|
|
@@ -1044,6 +1074,7 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
1044
1074
|
"\u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439",
|
|
1045
1075
|
"\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u044F",
|
|
1046
1076
|
"\u043A\u043E\u0434",
|
|
1077
|
+
"\u0440\u0430\u043D\u0435\u0435",
|
|
1047
1078
|
"\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435"
|
|
1048
1079
|
],
|
|
1049
1080
|
negationKeywords: [
|
|
@@ -1071,11 +1102,14 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
1071
1102
|
"\u9664\u304F",
|
|
1072
1103
|
// Russian
|
|
1073
1104
|
"\u043D\u0435 \u0434\u0435\u043B\u0430\u0439",
|
|
1105
|
+
"\u043D\u0435 \u043D\u0430\u0434\u043E",
|
|
1106
|
+
"\u043D\u0435\u043B\u044C\u0437\u044F",
|
|
1074
1107
|
"\u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044C",
|
|
1075
1108
|
"\u043D\u0438\u043A\u043E\u0433\u0434\u0430",
|
|
1076
1109
|
"\u0431\u0435\u0437",
|
|
1077
1110
|
"\u043A\u0440\u043E\u043C\u0435",
|
|
1078
|
-
"\u0438\u0441\u043A\u043B\u044E\u0447\u0438\u0442\u044C"
|
|
1111
|
+
"\u0438\u0441\u043A\u043B\u044E\u0447\u0438\u0442\u044C",
|
|
1112
|
+
"\u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435"
|
|
1079
1113
|
],
|
|
1080
1114
|
domainSpecificKeywords: [
|
|
1081
1115
|
// English
|
|
@@ -1112,7 +1146,8 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
1112
1146
|
"\u043F\u0440\u043E\u0442\u0435\u043E\u043C\u0438\u043A\u0430",
|
|
1113
1147
|
"\u0442\u043E\u043F\u043E\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438\u0439",
|
|
1114
1148
|
"\u0433\u043E\u043C\u043E\u043C\u043E\u0440\u0444\u043D\u044B\u0439",
|
|
1115
|
-
"\u0441 \u043D\u0443\u043B\u0435\u0432\u044B\u043C \u0440\u0430\u0437\u0433\u043B\u0430\u0448\u0435\u043D\u0438\u0435\u043C"
|
|
1149
|
+
"\u0441 \u043D\u0443\u043B\u0435\u0432\u044B\u043C \u0440\u0430\u0437\u0433\u043B\u0430\u0448\u0435\u043D\u0438\u0435\u043C",
|
|
1150
|
+
"\u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0440\u0435\u0448\u0451\u0442\u043E\u043A"
|
|
1116
1151
|
],
|
|
1117
1152
|
// Dimension weights (sum to 1.0)
|
|
1118
1153
|
dimensionWeights: {
|
|
@@ -1547,6 +1582,119 @@ var AUTO_MODEL_SHORT = "auto";
|
|
|
1547
1582
|
var HEARTBEAT_INTERVAL_MS = 2e3;
|
|
1548
1583
|
var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
|
|
1549
1584
|
var DEFAULT_PORT = 8402;
|
|
1585
|
+
var MAX_FALLBACK_ATTEMPTS = 3;
|
|
1586
|
+
var HEALTH_CHECK_TIMEOUT_MS = 2e3;
|
|
1587
|
+
function getProxyPort() {
|
|
1588
|
+
const envPort = process.env.BLOCKRUN_PROXY_PORT;
|
|
1589
|
+
if (envPort) {
|
|
1590
|
+
const parsed = parseInt(envPort, 10);
|
|
1591
|
+
if (!isNaN(parsed) && parsed > 0 && parsed < 65536) {
|
|
1592
|
+
return parsed;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
return DEFAULT_PORT;
|
|
1596
|
+
}
|
|
1597
|
+
async function checkExistingProxy(port) {
|
|
1598
|
+
const controller = new AbortController();
|
|
1599
|
+
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
|
|
1600
|
+
try {
|
|
1601
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
1602
|
+
signal: controller.signal
|
|
1603
|
+
});
|
|
1604
|
+
clearTimeout(timeoutId);
|
|
1605
|
+
if (response.ok) {
|
|
1606
|
+
const data = await response.json();
|
|
1607
|
+
if (data.status === "ok" && data.wallet) {
|
|
1608
|
+
return data.wallet;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return void 0;
|
|
1612
|
+
} catch {
|
|
1613
|
+
clearTimeout(timeoutId);
|
|
1614
|
+
return void 0;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
var PROVIDER_ERROR_PATTERNS = [
|
|
1618
|
+
/billing/i,
|
|
1619
|
+
/insufficient.*balance/i,
|
|
1620
|
+
/credits/i,
|
|
1621
|
+
/quota.*exceeded/i,
|
|
1622
|
+
/rate.*limit/i,
|
|
1623
|
+
/model.*unavailable/i,
|
|
1624
|
+
/model.*not.*available/i,
|
|
1625
|
+
/service.*unavailable/i,
|
|
1626
|
+
/capacity/i,
|
|
1627
|
+
/overloaded/i,
|
|
1628
|
+
/temporarily.*unavailable/i,
|
|
1629
|
+
/api.*key.*invalid/i,
|
|
1630
|
+
/authentication.*failed/i
|
|
1631
|
+
];
|
|
1632
|
+
var FALLBACK_STATUS_CODES = [
|
|
1633
|
+
400,
|
|
1634
|
+
// Bad request - sometimes used for billing errors
|
|
1635
|
+
401,
|
|
1636
|
+
// Unauthorized - provider API key issues
|
|
1637
|
+
402,
|
|
1638
|
+
// Payment required - but from upstream, not x402
|
|
1639
|
+
403,
|
|
1640
|
+
// Forbidden - provider restrictions
|
|
1641
|
+
429,
|
|
1642
|
+
// Rate limited
|
|
1643
|
+
500,
|
|
1644
|
+
// Internal server error
|
|
1645
|
+
502,
|
|
1646
|
+
// Bad gateway
|
|
1647
|
+
503,
|
|
1648
|
+
// Service unavailable
|
|
1649
|
+
504
|
|
1650
|
+
// Gateway timeout
|
|
1651
|
+
];
|
|
1652
|
+
function isProviderError(status, body) {
|
|
1653
|
+
if (!FALLBACK_STATUS_CODES.includes(status)) {
|
|
1654
|
+
return false;
|
|
1655
|
+
}
|
|
1656
|
+
if (status >= 500) {
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
return PROVIDER_ERROR_PATTERNS.some((pattern) => pattern.test(body));
|
|
1660
|
+
}
|
|
1661
|
+
function normalizeMessagesForGoogle(messages) {
|
|
1662
|
+
if (!messages || messages.length === 0) return messages;
|
|
1663
|
+
let firstNonSystemIdx = -1;
|
|
1664
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1665
|
+
if (messages[i].role !== "system") {
|
|
1666
|
+
firstNonSystemIdx = i;
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
if (firstNonSystemIdx === -1) return messages;
|
|
1671
|
+
const firstRole = messages[firstNonSystemIdx].role;
|
|
1672
|
+
if (firstRole === "user") return messages;
|
|
1673
|
+
if (firstRole === "assistant" || firstRole === "model") {
|
|
1674
|
+
const normalized = [...messages];
|
|
1675
|
+
normalized.splice(firstNonSystemIdx, 0, {
|
|
1676
|
+
role: "user",
|
|
1677
|
+
content: "(continuing conversation)"
|
|
1678
|
+
});
|
|
1679
|
+
return normalized;
|
|
1680
|
+
}
|
|
1681
|
+
return messages;
|
|
1682
|
+
}
|
|
1683
|
+
function isGoogleModel(modelId) {
|
|
1684
|
+
return modelId.startsWith("google/") || modelId.startsWith("gemini");
|
|
1685
|
+
}
|
|
1686
|
+
var KIMI_BLOCK_RE = /<[||][^<>]*begin[^<>]*[||]>[\s\S]*?<[||][^<>]*end[^<>]*[||]>/gi;
|
|
1687
|
+
var KIMI_TOKEN_RE = /<[||][^<>]*[||]>/g;
|
|
1688
|
+
var THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>/gi;
|
|
1689
|
+
var THINKING_BLOCK_RE = /<\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>[\s\S]*?<\s*\/\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
|
|
1690
|
+
function stripThinkingTokens(content) {
|
|
1691
|
+
if (!content) return content;
|
|
1692
|
+
let cleaned = content.replace(KIMI_BLOCK_RE, "");
|
|
1693
|
+
cleaned = cleaned.replace(KIMI_TOKEN_RE, "");
|
|
1694
|
+
cleaned = cleaned.replace(THINKING_BLOCK_RE, "");
|
|
1695
|
+
cleaned = cleaned.replace(THINKING_TAG_RE, "");
|
|
1696
|
+
return cleaned;
|
|
1697
|
+
}
|
|
1550
1698
|
function buildModelPricing() {
|
|
1551
1699
|
const map = /* @__PURE__ */ new Map();
|
|
1552
1700
|
for (const m of BLOCKRUN_MODELS) {
|
|
@@ -1577,6 +1725,27 @@ function estimateAmount(modelId, bodyLength, maxTokens) {
|
|
|
1577
1725
|
}
|
|
1578
1726
|
async function startProxy(options) {
|
|
1579
1727
|
const apiBase = options.apiBase ?? BLOCKRUN_API;
|
|
1728
|
+
const listenPort = options.port ?? getProxyPort();
|
|
1729
|
+
const existingWallet = await checkExistingProxy(listenPort);
|
|
1730
|
+
if (existingWallet) {
|
|
1731
|
+
const account2 = privateKeyToAccount2(options.walletKey);
|
|
1732
|
+
const balanceMonitor2 = new BalanceMonitor(account2.address);
|
|
1733
|
+
const baseUrl = `http://127.0.0.1:${listenPort}`;
|
|
1734
|
+
if (existingWallet !== account2.address) {
|
|
1735
|
+
console.warn(
|
|
1736
|
+
`[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingWallet}, but current config uses ${account2.address}. Reusing existing proxy.`
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
options.onReady?.(listenPort);
|
|
1740
|
+
return {
|
|
1741
|
+
port: listenPort,
|
|
1742
|
+
baseUrl,
|
|
1743
|
+
walletAddress: existingWallet,
|
|
1744
|
+
balanceMonitor: balanceMonitor2,
|
|
1745
|
+
close: async () => {
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1580
1749
|
const account = privateKeyToAccount2(options.walletKey);
|
|
1581
1750
|
const { fetch: payFetch } = createPaymentFetch(options.walletKey);
|
|
1582
1751
|
const balanceMonitor = new BalanceMonitor(account.address);
|
|
@@ -1646,7 +1815,6 @@ async function startProxy(options) {
|
|
|
1646
1815
|
}
|
|
1647
1816
|
}
|
|
1648
1817
|
});
|
|
1649
|
-
const listenPort = options.port ?? DEFAULT_PORT;
|
|
1650
1818
|
return new Promise((resolve, reject) => {
|
|
1651
1819
|
server.on("error", reject);
|
|
1652
1820
|
server.listen(listenPort, "127.0.0.1", () => {
|
|
@@ -1666,6 +1834,52 @@ async function startProxy(options) {
|
|
|
1666
1834
|
});
|
|
1667
1835
|
});
|
|
1668
1836
|
}
|
|
1837
|
+
async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxTokens, payFetch, balanceMonitor, signal) {
|
|
1838
|
+
let requestBody = body;
|
|
1839
|
+
try {
|
|
1840
|
+
const parsed = JSON.parse(body.toString());
|
|
1841
|
+
parsed.model = modelId;
|
|
1842
|
+
if (isGoogleModel(modelId) && Array.isArray(parsed.messages)) {
|
|
1843
|
+
parsed.messages = normalizeMessagesForGoogle(parsed.messages);
|
|
1844
|
+
}
|
|
1845
|
+
requestBody = Buffer.from(JSON.stringify(parsed));
|
|
1846
|
+
} catch {
|
|
1847
|
+
}
|
|
1848
|
+
const estimated = estimateAmount(modelId, requestBody.length, maxTokens);
|
|
1849
|
+
const preAuth = estimated ? { estimatedAmount: estimated } : void 0;
|
|
1850
|
+
try {
|
|
1851
|
+
const response = await payFetch(
|
|
1852
|
+
upstreamUrl,
|
|
1853
|
+
{
|
|
1854
|
+
method,
|
|
1855
|
+
headers,
|
|
1856
|
+
body: requestBody.length > 0 ? new Uint8Array(requestBody) : void 0,
|
|
1857
|
+
signal
|
|
1858
|
+
},
|
|
1859
|
+
preAuth
|
|
1860
|
+
);
|
|
1861
|
+
if (response.status !== 200) {
|
|
1862
|
+
const errorBody = await response.text();
|
|
1863
|
+
const isProviderErr = isProviderError(response.status, errorBody);
|
|
1864
|
+
return {
|
|
1865
|
+
success: false,
|
|
1866
|
+
errorBody,
|
|
1867
|
+
errorStatus: response.status,
|
|
1868
|
+
isProviderError: isProviderErr
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
return { success: true, response };
|
|
1872
|
+
} catch (err) {
|
|
1873
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1874
|
+
return {
|
|
1875
|
+
success: false,
|
|
1876
|
+
errorBody: errorMsg,
|
|
1877
|
+
errorStatus: 500,
|
|
1878
|
+
isProviderError: true
|
|
1879
|
+
// Network errors are retryable
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1669
1883
|
async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor) {
|
|
1670
1884
|
const startTime = Date.now();
|
|
1671
1885
|
const upstreamUrl = `${apiBase}${req.url}`;
|
|
@@ -1740,7 +1954,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
1740
1954
|
}
|
|
1741
1955
|
deduplicator.markInflight(dedupKey);
|
|
1742
1956
|
let estimatedCostMicros;
|
|
1743
|
-
if (modelId) {
|
|
1957
|
+
if (modelId && !options.skipBalanceCheck) {
|
|
1744
1958
|
const estimated = estimateAmount(modelId, body.length, maxTokens);
|
|
1745
1959
|
if (estimated) {
|
|
1746
1960
|
estimatedCostMicros = BigInt(estimated);
|
|
@@ -1805,10 +2019,6 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
1805
2019
|
headers["content-type"] = "application/json";
|
|
1806
2020
|
}
|
|
1807
2021
|
headers["user-agent"] = USER_AGENT;
|
|
1808
|
-
let preAuth;
|
|
1809
|
-
if (estimatedCostMicros !== void 0) {
|
|
1810
|
-
preAuth = { estimatedAmount: estimatedCostMicros.toString() };
|
|
1811
|
-
}
|
|
1812
2022
|
let completed = false;
|
|
1813
2023
|
res.on("close", () => {
|
|
1814
2024
|
if (heartbeatInterval) {
|
|
@@ -1823,26 +2033,72 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
1823
2033
|
const controller = new AbortController();
|
|
1824
2034
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1825
2035
|
try {
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2036
|
+
let modelsToTry;
|
|
2037
|
+
if (routingDecision) {
|
|
2038
|
+
modelsToTry = getFallbackChain(routingDecision.tier, routerOpts.config.tiers);
|
|
2039
|
+
modelsToTry = modelsToTry.slice(0, MAX_FALLBACK_ATTEMPTS);
|
|
2040
|
+
} else {
|
|
2041
|
+
modelsToTry = modelId ? [modelId] : [];
|
|
2042
|
+
}
|
|
2043
|
+
let upstream;
|
|
2044
|
+
let lastError;
|
|
2045
|
+
let actualModelUsed = modelId;
|
|
2046
|
+
for (let i = 0; i < modelsToTry.length; i++) {
|
|
2047
|
+
const tryModel = modelsToTry[i];
|
|
2048
|
+
const isLastAttempt = i === modelsToTry.length - 1;
|
|
2049
|
+
console.log(`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`);
|
|
2050
|
+
const result = await tryModelRequest(
|
|
2051
|
+
upstreamUrl,
|
|
2052
|
+
req.method ?? "POST",
|
|
1830
2053
|
headers,
|
|
1831
|
-
body
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2054
|
+
body,
|
|
2055
|
+
tryModel,
|
|
2056
|
+
maxTokens,
|
|
2057
|
+
payFetch,
|
|
2058
|
+
balanceMonitor,
|
|
2059
|
+
controller.signal
|
|
2060
|
+
);
|
|
2061
|
+
if (result.success && result.response) {
|
|
2062
|
+
upstream = result.response;
|
|
2063
|
+
actualModelUsed = tryModel;
|
|
2064
|
+
console.log(`[ClawRouter] Success with model: ${tryModel}`);
|
|
2065
|
+
break;
|
|
2066
|
+
}
|
|
2067
|
+
lastError = {
|
|
2068
|
+
body: result.errorBody || "Unknown error",
|
|
2069
|
+
status: result.errorStatus || 500
|
|
2070
|
+
};
|
|
2071
|
+
if (result.isProviderError && !isLastAttempt) {
|
|
2072
|
+
console.log(
|
|
2073
|
+
`[ClawRouter] Provider error from ${tryModel}, trying fallback: ${result.errorBody?.slice(0, 100)}`
|
|
2074
|
+
);
|
|
2075
|
+
continue;
|
|
2076
|
+
}
|
|
2077
|
+
if (!result.isProviderError) {
|
|
2078
|
+
console.log(
|
|
2079
|
+
`[ClawRouter] Non-provider error from ${tryModel}, not retrying: ${result.errorBody?.slice(0, 100)}`
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
break;
|
|
2083
|
+
}
|
|
1836
2084
|
clearTimeout(timeoutId);
|
|
1837
2085
|
if (heartbeatInterval) {
|
|
1838
2086
|
clearInterval(heartbeatInterval);
|
|
1839
2087
|
heartbeatInterval = void 0;
|
|
1840
2088
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2089
|
+
if (routingDecision && actualModelUsed !== routingDecision.model) {
|
|
2090
|
+
routingDecision = {
|
|
2091
|
+
...routingDecision,
|
|
2092
|
+
model: actualModelUsed,
|
|
2093
|
+
reasoning: `${routingDecision.reasoning} | fallback to ${actualModelUsed}`
|
|
2094
|
+
};
|
|
2095
|
+
options.onRouted?.(routingDecision);
|
|
2096
|
+
}
|
|
2097
|
+
if (!upstream) {
|
|
2098
|
+
const errBody = lastError?.body || "All models in fallback chain failed";
|
|
2099
|
+
const errStatus = lastError?.status || 502;
|
|
2100
|
+
if (headersSentEarly) {
|
|
2101
|
+
const errEvent = `data: ${JSON.stringify({ error: { message: errBody, type: "provider_error", status: errStatus } })}
|
|
1846
2102
|
|
|
1847
2103
|
`;
|
|
1848
2104
|
res.write(errEvent);
|
|
@@ -1851,13 +2107,30 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
1851
2107
|
const errBuf = Buffer.from(errEvent + "data: [DONE]\n\n");
|
|
1852
2108
|
deduplicator.complete(dedupKey, {
|
|
1853
2109
|
status: 200,
|
|
1854
|
-
// we already sent 200
|
|
1855
2110
|
headers: { "content-type": "text/event-stream" },
|
|
1856
2111
|
body: errBuf,
|
|
1857
2112
|
completedAt: Date.now()
|
|
1858
2113
|
});
|
|
1859
|
-
|
|
2114
|
+
} else {
|
|
2115
|
+
res.writeHead(errStatus, { "Content-Type": "application/json" });
|
|
2116
|
+
res.end(
|
|
2117
|
+
JSON.stringify({
|
|
2118
|
+
error: { message: errBody, type: "provider_error" }
|
|
2119
|
+
})
|
|
2120
|
+
);
|
|
2121
|
+
deduplicator.complete(dedupKey, {
|
|
2122
|
+
status: errStatus,
|
|
2123
|
+
headers: { "content-type": "application/json" },
|
|
2124
|
+
body: Buffer.from(
|
|
2125
|
+
JSON.stringify({ error: { message: errBody, type: "provider_error" } })
|
|
2126
|
+
),
|
|
2127
|
+
completedAt: Date.now()
|
|
2128
|
+
});
|
|
1860
2129
|
}
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
const responseChunks = [];
|
|
2133
|
+
if (headersSentEarly) {
|
|
1861
2134
|
if (upstream.body) {
|
|
1862
2135
|
const reader = upstream.body.getReader();
|
|
1863
2136
|
const chunks = [];
|
|
@@ -1882,7 +2155,8 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
1882
2155
|
};
|
|
1883
2156
|
if (rsp.choices && Array.isArray(rsp.choices)) {
|
|
1884
2157
|
for (const choice of rsp.choices) {
|
|
1885
|
-
const
|
|
2158
|
+
const rawContent = choice.message?.content ?? choice.delta?.content ?? "";
|
|
2159
|
+
const content = stripThinkingTokens(rawContent);
|
|
1886
2160
|
const role = choice.message?.role ?? choice.delta?.role ?? "assistant";
|
|
1887
2161
|
const index = choice.index ?? 0;
|
|
1888
2162
|
const roleChunk = {
|
|
@@ -2104,13 +2378,18 @@ function injectModelsConfig(logger) {
|
|
|
2104
2378
|
let needsWrite = false;
|
|
2105
2379
|
if (!config.models) config.models = {};
|
|
2106
2380
|
if (!config.models.providers) config.models.providers = {};
|
|
2381
|
+
const proxyPort = getProxyPort();
|
|
2382
|
+
const expectedBaseUrl = `http://127.0.0.1:${proxyPort}/v1`;
|
|
2107
2383
|
if (!config.models.providers.blockrun) {
|
|
2108
2384
|
config.models.providers.blockrun = {
|
|
2109
|
-
baseUrl:
|
|
2385
|
+
baseUrl: expectedBaseUrl,
|
|
2110
2386
|
api: "openai-completions",
|
|
2111
2387
|
models: OPENCLAW_MODELS
|
|
2112
2388
|
};
|
|
2113
2389
|
needsWrite = true;
|
|
2390
|
+
} else if (config.models.providers.blockrun.baseUrl !== expectedBaseUrl) {
|
|
2391
|
+
config.models.providers.blockrun.baseUrl = expectedBaseUrl;
|
|
2392
|
+
needsWrite = true;
|
|
2114
2393
|
}
|
|
2115
2394
|
if (!config.agents) config.agents = {};
|
|
2116
2395
|
if (!config.agents.defaults) config.agents.defaults = {};
|
|
@@ -2259,6 +2538,7 @@ var plugin = {
|
|
|
2259
2538
|
api.registerProvider(blockrunProvider);
|
|
2260
2539
|
injectModelsConfig(api.logger);
|
|
2261
2540
|
injectAuthProfile(api.logger);
|
|
2541
|
+
const runtimePort = getProxyPort();
|
|
2262
2542
|
if (!api.config.models) {
|
|
2263
2543
|
api.config.models = { providers: {} };
|
|
2264
2544
|
}
|
|
@@ -2266,7 +2546,7 @@ var plugin = {
|
|
|
2266
2546
|
api.config.models.providers = {};
|
|
2267
2547
|
}
|
|
2268
2548
|
api.config.models.providers.blockrun = {
|
|
2269
|
-
baseUrl:
|
|
2549
|
+
baseUrl: `http://127.0.0.1:${runtimePort}/v1`,
|
|
2270
2550
|
api: "openai-completions",
|
|
2271
2551
|
models: OPENCLAW_MODELS
|
|
2272
2552
|
};
|
|
@@ -2320,6 +2600,7 @@ export {
|
|
|
2320
2600
|
createPaymentFetch,
|
|
2321
2601
|
index_default as default,
|
|
2322
2602
|
fetchWithRetry,
|
|
2603
|
+
getProxyPort,
|
|
2323
2604
|
isBalanceError,
|
|
2324
2605
|
isEmptyWalletError,
|
|
2325
2606
|
isInsufficientFundsError,
|