@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 CHANGED
@@ -12,7 +12,7 @@ One wallet, 30+ models, zero API keys.
12
12
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](https://typescriptlang.org)
13
13
  [![Node](https://img.shields.io/badge/node-%E2%89%A520-brightgreen.svg)](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
- ### Source Structure
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
- ### Override Tier Models
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
- ### Override Scoring Weights
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
- "\u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C"
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
- const upstream = await payFetch(
1827
- upstreamUrl,
1828
- {
1829
- method: req.method ?? "POST",
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: body.length > 0 ? body : void 0,
1832
- signal: controller.signal
1833
- },
1834
- preAuth
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
- const responseChunks = [];
1842
- if (headersSentEarly) {
1843
- if (upstream.status !== 200) {
1844
- const errBody = await upstream.text();
1845
- const errEvent = `data: ${JSON.stringify({ error: { message: errBody, type: "upstream_error", status: upstream.status } })}
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
- return;
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 content = choice.message?.content ?? choice.delta?.content ?? "";
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: "http://127.0.0.1:8402/v1",
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: "http://127.0.0.1:8402/v1",
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,