@blockrun/clawrouter 0.6.9 → 0.8.0

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/dist/index.js CHANGED
@@ -2324,6 +2324,8 @@ var DEFAULT_PORT = 8402;
2324
2324
  var MAX_FALLBACK_ATTEMPTS = 3;
2325
2325
  var HEALTH_CHECK_TIMEOUT_MS = 2e3;
2326
2326
  var RATE_LIMIT_COOLDOWN_MS = 6e4;
2327
+ var PORT_RETRY_ATTEMPTS = 5;
2328
+ var PORT_RETRY_DELAY_MS = 1e3;
2327
2329
  var rateLimitedModels = /* @__PURE__ */ new Map();
2328
2330
  function isRateLimited(modelId) {
2329
2331
  const hitTime = rateLimitedModels.get(modelId);
@@ -2613,7 +2615,7 @@ async function startProxy(options) {
2613
2615
  if (existingWallet) {
2614
2616
  const account2 = privateKeyToAccount2(options.walletKey);
2615
2617
  const balanceMonitor2 = new BalanceMonitor(account2.address);
2616
- const baseUrl = `http://127.0.0.1:${listenPort}`;
2618
+ const baseUrl2 = `http://127.0.0.1:${listenPort}`;
2617
2619
  if (existingWallet !== account2.address) {
2618
2620
  console.warn(
2619
2621
  `[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingWallet}, but current config uses ${account2.address}. Reusing existing proxy.`
@@ -2622,7 +2624,7 @@ async function startProxy(options) {
2622
2624
  options.onReady?.(listenPort);
2623
2625
  return {
2624
2626
  port: listenPort,
2625
- baseUrl,
2627
+ baseUrl: baseUrl2,
2626
2628
  walletAddress: existingWallet,
2627
2629
  balanceMonitor: balanceMonitor2,
2628
2630
  close: async () => {
@@ -2748,80 +2750,123 @@ async function startProxy(options) {
2748
2750
  }
2749
2751
  }
2750
2752
  });
2751
- return new Promise((resolve, reject) => {
2752
- server.on("error", (err) => {
2753
- if (err.code === "EADDRINUSE") {
2754
- const baseUrl = `http://127.0.0.1:${listenPort}`;
2753
+ const tryListen = (attempt) => {
2754
+ return new Promise((resolveAttempt, rejectAttempt) => {
2755
+ const onError = async (err) => {
2756
+ server.removeListener("error", onError);
2757
+ if (err.code === "EADDRINUSE") {
2758
+ const existingWallet2 = await checkExistingProxy(listenPort);
2759
+ if (existingWallet2) {
2760
+ console.log(`[ClawRouter] Existing proxy detected on port ${listenPort}, reusing`);
2761
+ rejectAttempt({ code: "REUSE_EXISTING", wallet: existingWallet2 });
2762
+ return;
2763
+ }
2764
+ if (attempt < PORT_RETRY_ATTEMPTS) {
2765
+ console.log(
2766
+ `[ClawRouter] Port ${listenPort} in TIME_WAIT, retrying in ${PORT_RETRY_DELAY_MS}ms (attempt ${attempt}/${PORT_RETRY_ATTEMPTS})`
2767
+ );
2768
+ rejectAttempt({ code: "RETRY", attempt });
2769
+ return;
2770
+ }
2771
+ console.error(
2772
+ `[ClawRouter] Port ${listenPort} still in use after ${PORT_RETRY_ATTEMPTS} attempts`
2773
+ );
2774
+ rejectAttempt(err);
2775
+ return;
2776
+ }
2777
+ rejectAttempt(err);
2778
+ };
2779
+ server.once("error", onError);
2780
+ server.listen(listenPort, "127.0.0.1", () => {
2781
+ server.removeListener("error", onError);
2782
+ resolveAttempt();
2783
+ });
2784
+ });
2785
+ };
2786
+ let lastError;
2787
+ for (let attempt = 1; attempt <= PORT_RETRY_ATTEMPTS; attempt++) {
2788
+ try {
2789
+ await tryListen(attempt);
2790
+ break;
2791
+ } catch (err) {
2792
+ const error = err;
2793
+ if (error.code === "REUSE_EXISTING" && error.wallet) {
2794
+ const baseUrl2 = `http://127.0.0.1:${listenPort}`;
2755
2795
  options.onReady?.(listenPort);
2756
- resolve({
2796
+ return {
2757
2797
  port: listenPort,
2758
- baseUrl,
2759
- walletAddress: account.address,
2798
+ baseUrl: baseUrl2,
2799
+ walletAddress: error.wallet,
2760
2800
  balanceMonitor,
2761
2801
  close: async () => {
2762
2802
  }
2763
- });
2764
- return;
2803
+ };
2765
2804
  }
2766
- reject(err);
2805
+ if (error.code === "RETRY") {
2806
+ await new Promise((r) => setTimeout(r, PORT_RETRY_DELAY_MS));
2807
+ continue;
2808
+ }
2809
+ lastError = err;
2810
+ break;
2811
+ }
2812
+ }
2813
+ if (lastError) {
2814
+ throw lastError;
2815
+ }
2816
+ const addr = server.address();
2817
+ const port = addr.port;
2818
+ const baseUrl = `http://127.0.0.1:${port}`;
2819
+ options.onReady?.(port);
2820
+ server.on("error", (err) => {
2821
+ console.error(`[ClawRouter] Server runtime error: ${err.message}`);
2822
+ options.onError?.(err);
2823
+ });
2824
+ server.on("clientError", (err, socket) => {
2825
+ console.error(`[ClawRouter] Client error: ${err.message}`);
2826
+ if (socket.writable && !socket.destroyed) {
2827
+ socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
2828
+ }
2829
+ });
2830
+ server.on("connection", (socket) => {
2831
+ connections.add(socket);
2832
+ socket.setTimeout(3e5);
2833
+ socket.on("timeout", () => {
2834
+ console.error(`[ClawRouter] Socket timeout, destroying connection`);
2835
+ socket.destroy();
2767
2836
  });
2768
- server.listen(listenPort, "127.0.0.1", () => {
2769
- const addr = server.address();
2770
- const port = addr.port;
2771
- const baseUrl = `http://127.0.0.1:${port}`;
2772
- options.onReady?.(port);
2773
- server.on("error", (err) => {
2774
- console.error(`[ClawRouter] Server runtime error: ${err.message}`);
2775
- options.onError?.(err);
2776
- });
2777
- server.on("clientError", (err, socket) => {
2778
- console.error(`[ClawRouter] Client error: ${err.message}`);
2779
- if (socket.writable && !socket.destroyed) {
2780
- socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
2781
- }
2782
- });
2783
- server.on("connection", (socket) => {
2784
- connections.add(socket);
2785
- socket.setTimeout(3e5);
2786
- socket.on("timeout", () => {
2787
- console.error(`[ClawRouter] Socket timeout, destroying connection`);
2788
- socket.destroy();
2789
- });
2790
- socket.on("end", () => {
2791
- });
2792
- socket.on("error", (err) => {
2793
- console.error(`[ClawRouter] Socket error: ${err.message}`);
2794
- });
2795
- socket.on("close", () => {
2796
- connections.delete(socket);
2797
- });
2798
- });
2799
- resolve({
2800
- port,
2801
- baseUrl,
2802
- walletAddress: account.address,
2803
- balanceMonitor,
2804
- close: () => new Promise((res, rej) => {
2805
- const timeout = setTimeout(() => {
2806
- rej(new Error("[ClawRouter] Close timeout after 4s"));
2807
- }, 4e3);
2808
- sessionStore.close();
2809
- for (const socket of connections) {
2810
- socket.destroy();
2811
- }
2812
- connections.clear();
2813
- server.close((err) => {
2814
- clearTimeout(timeout);
2815
- if (err) {
2816
- rej(err);
2817
- } else {
2818
- res();
2819
- }
2820
- });
2821
- })
2822
- });
2837
+ socket.on("end", () => {
2838
+ });
2839
+ socket.on("error", (err) => {
2840
+ console.error(`[ClawRouter] Socket error: ${err.message}`);
2841
+ });
2842
+ socket.on("close", () => {
2843
+ connections.delete(socket);
2823
2844
  });
2824
2845
  });
2846
+ return {
2847
+ port,
2848
+ baseUrl,
2849
+ walletAddress: account.address,
2850
+ balanceMonitor,
2851
+ close: () => new Promise((res, rej) => {
2852
+ const timeout = setTimeout(() => {
2853
+ rej(new Error("[ClawRouter] Close timeout after 4s"));
2854
+ }, 4e3);
2855
+ sessionStore.close();
2856
+ for (const socket of connections) {
2857
+ socket.destroy();
2858
+ }
2859
+ connections.clear();
2860
+ server.close((err) => {
2861
+ clearTimeout(timeout);
2862
+ if (err) {
2863
+ rej(err);
2864
+ } else {
2865
+ res();
2866
+ }
2867
+ });
2868
+ })
2869
+ };
2825
2870
  }
2826
2871
  async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxTokens, payFetch, balanceMonitor, signal) {
2827
2872
  let requestBody = body;
@@ -3457,80 +3502,137 @@ function isCompletionMode() {
3457
3502
  return args.some((arg, i) => arg === "completion" && i >= 1 && i <= 3);
3458
3503
  }
3459
3504
  function injectModelsConfig(logger) {
3460
- const configPath = join5(homedir4(), ".openclaw", "openclaw.json");
3461
- if (!existsSync(configPath)) {
3462
- logger.info("OpenClaw config not found, skipping models injection");
3463
- return;
3505
+ const configDir = join5(homedir4(), ".openclaw");
3506
+ const configPath = join5(configDir, "openclaw.json");
3507
+ let config = {};
3508
+ let needsWrite = false;
3509
+ if (!existsSync(configDir)) {
3510
+ try {
3511
+ mkdirSync(configDir, { recursive: true });
3512
+ logger.info("Created OpenClaw config directory");
3513
+ } catch (err) {
3514
+ logger.info(`Failed to create config dir: ${err instanceof Error ? err.message : String(err)}`);
3515
+ return;
3516
+ }
3464
3517
  }
3465
- try {
3466
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
3467
- let needsWrite = false;
3468
- if (!config.models) config.models = {};
3469
- if (!config.models.providers) config.models.providers = {};
3470
- const proxyPort = getProxyPort();
3471
- const expectedBaseUrl = `http://127.0.0.1:${proxyPort}/v1`;
3472
- if (!config.models.providers.blockrun) {
3473
- config.models.providers.blockrun = {
3474
- baseUrl: expectedBaseUrl,
3475
- api: "openai-completions",
3476
- // apiKey is required by pi-coding-agent's ModelRegistry for providers with models.
3477
- // We use a placeholder since the proxy handles real x402 auth internally.
3478
- apiKey: "x402-proxy-handles-auth",
3479
- models: OPENCLAW_MODELS
3480
- };
3481
- needsWrite = true;
3482
- } else {
3483
- if (config.models.providers.blockrun.baseUrl !== expectedBaseUrl) {
3484
- config.models.providers.blockrun.baseUrl = expectedBaseUrl;
3485
- needsWrite = true;
3486
- }
3487
- if (!config.models.providers.blockrun.apiKey) {
3488
- config.models.providers.blockrun.apiKey = "x402-proxy-handles-auth";
3489
- needsWrite = true;
3490
- }
3491
- const currentModels = config.models.providers.blockrun.models;
3492
- if (!currentModels || currentModels.length !== OPENCLAW_MODELS.length) {
3493
- config.models.providers.blockrun.models = OPENCLAW_MODELS;
3518
+ if (existsSync(configPath)) {
3519
+ try {
3520
+ const content = readFileSync(configPath, "utf-8").trim();
3521
+ if (content) {
3522
+ config = JSON.parse(content);
3523
+ } else {
3524
+ logger.info("OpenClaw config is empty, initializing");
3494
3525
  needsWrite = true;
3495
3526
  }
3527
+ } catch (err) {
3528
+ logger.info(`Failed to parse config (will recreate): ${err instanceof Error ? err.message : String(err)}`);
3529
+ config = {};
3530
+ needsWrite = true;
3496
3531
  }
3497
- if (!config.agents) config.agents = {};
3498
- if (!config.agents.defaults) config.agents.defaults = {};
3499
- if (!config.agents.defaults.model) config.agents.defaults.model = {};
3500
- if (config.agents.defaults.model.primary !== "blockrun/auto") {
3501
- config.agents.defaults.model.primary = "blockrun/auto";
3532
+ } else {
3533
+ logger.info("OpenClaw config not found, creating");
3534
+ needsWrite = true;
3535
+ }
3536
+ if (!config.models) {
3537
+ config.models = {};
3538
+ needsWrite = true;
3539
+ }
3540
+ const models = config.models;
3541
+ if (!models.providers) {
3542
+ models.providers = {};
3543
+ needsWrite = true;
3544
+ }
3545
+ const proxyPort = getProxyPort();
3546
+ const expectedBaseUrl = `http://127.0.0.1:${proxyPort}/v1`;
3547
+ const providers = models.providers;
3548
+ if (!providers.blockrun) {
3549
+ providers.blockrun = {
3550
+ baseUrl: expectedBaseUrl,
3551
+ api: "openai-completions",
3552
+ // apiKey is required by pi-coding-agent's ModelRegistry for providers with models.
3553
+ // We use a placeholder since the proxy handles real x402 auth internally.
3554
+ apiKey: "x402-proxy-handles-auth",
3555
+ models: OPENCLAW_MODELS
3556
+ };
3557
+ logger.info("Injected BlockRun provider config");
3558
+ needsWrite = true;
3559
+ } else {
3560
+ const blockrun = providers.blockrun;
3561
+ let fixed = false;
3562
+ if (!blockrun.baseUrl || blockrun.baseUrl !== expectedBaseUrl) {
3563
+ blockrun.baseUrl = expectedBaseUrl;
3564
+ fixed = true;
3565
+ }
3566
+ if (!blockrun.api) {
3567
+ blockrun.api = "openai-completions";
3568
+ fixed = true;
3569
+ }
3570
+ if (!blockrun.apiKey) {
3571
+ blockrun.apiKey = "x402-proxy-handles-auth";
3572
+ fixed = true;
3573
+ }
3574
+ const currentModels = blockrun.models;
3575
+ if (!currentModels || !Array.isArray(currentModels) || currentModels.length !== OPENCLAW_MODELS.length) {
3576
+ blockrun.models = OPENCLAW_MODELS;
3577
+ fixed = true;
3578
+ }
3579
+ if (fixed) {
3580
+ logger.info("Fixed incomplete BlockRun provider config");
3502
3581
  needsWrite = true;
3503
3582
  }
3504
- const KEY_MODEL_ALIASES = [
3505
- { id: "auto", alias: "auto" },
3506
- { id: "free", alias: "free" },
3507
- { id: "sonnet", alias: "sonnet" },
3508
- { id: "opus", alias: "opus" },
3509
- { id: "haiku", alias: "haiku" },
3510
- { id: "grok", alias: "grok" },
3511
- { id: "deepseek", alias: "deepseek" },
3512
- { id: "kimi", alias: "kimi" },
3513
- { id: "gemini", alias: "gemini" },
3514
- { id: "flash", alias: "flash" },
3515
- { id: "gpt", alias: "gpt" },
3516
- { id: "reasoner", alias: "reasoner" }
3517
- ];
3518
- if (!config.agents) config.agents = {};
3519
- if (!config.agents.defaults) config.agents.defaults = {};
3520
- if (!config.agents.defaults.models) config.agents.defaults.models = {};
3521
- const allowlist = config.agents.defaults.models;
3522
- for (const m of KEY_MODEL_ALIASES) {
3523
- const fullId = `blockrun/${m.id}`;
3524
- if (!allowlist[fullId]) {
3525
- allowlist[fullId] = { alias: m.alias };
3526
- needsWrite = true;
3527
- }
3583
+ }
3584
+ if (!config.agents) {
3585
+ config.agents = {};
3586
+ needsWrite = true;
3587
+ }
3588
+ const agents = config.agents;
3589
+ if (!agents.defaults) {
3590
+ agents.defaults = {};
3591
+ needsWrite = true;
3592
+ }
3593
+ const defaults = agents.defaults;
3594
+ if (!defaults.model) {
3595
+ defaults.model = {};
3596
+ needsWrite = true;
3597
+ }
3598
+ const model = defaults.model;
3599
+ if (model.primary !== "blockrun/auto") {
3600
+ model.primary = "blockrun/auto";
3601
+ needsWrite = true;
3602
+ }
3603
+ const KEY_MODEL_ALIASES = [
3604
+ { id: "auto", alias: "auto" },
3605
+ { id: "free", alias: "free" },
3606
+ { id: "sonnet", alias: "sonnet" },
3607
+ { id: "opus", alias: "opus" },
3608
+ { id: "haiku", alias: "haiku" },
3609
+ { id: "grok", alias: "grok" },
3610
+ { id: "deepseek", alias: "deepseek" },
3611
+ { id: "kimi", alias: "kimi" },
3612
+ { id: "gemini", alias: "gemini" },
3613
+ { id: "flash", alias: "flash" },
3614
+ { id: "gpt", alias: "gpt" },
3615
+ { id: "reasoner", alias: "reasoner" }
3616
+ ];
3617
+ if (!defaults.models) {
3618
+ defaults.models = {};
3619
+ needsWrite = true;
3620
+ }
3621
+ const allowlist = defaults.models;
3622
+ for (const m of KEY_MODEL_ALIASES) {
3623
+ const fullId = `blockrun/${m.id}`;
3624
+ if (!allowlist[fullId]) {
3625
+ allowlist[fullId] = { alias: m.alias };
3626
+ needsWrite = true;
3528
3627
  }
3529
- if (needsWrite) {
3628
+ }
3629
+ if (needsWrite) {
3630
+ try {
3530
3631
  writeFileSync(configPath, JSON.stringify(config, null, 2));
3531
3632
  logger.info("Smart routing enabled (blockrun/auto)");
3633
+ } catch (err) {
3634
+ logger.info(`Failed to write config: ${err instanceof Error ? err.message : String(err)}`);
3532
3635
  }
3533
- } catch {
3534
3636
  }
3535
3637
  }
3536
3638
  function injectAuthProfile(logger) {