@blockrun/clawrouter 0.12.273 → 0.12.275
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/cli.js +195 -50
- package/dist/index.d.ts +23 -17
- package/dist/index.js +195 -50
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -32378,6 +32378,40 @@ function registerSpendPolicyHook(x402, control) {
|
|
|
32378
32378
|
}
|
|
32379
32379
|
});
|
|
32380
32380
|
}
|
|
32381
|
+
function settledChargeUsd(response) {
|
|
32382
|
+
const raw = response.headers.get("x-blockrun-cost-usd");
|
|
32383
|
+
if (raw === null) return void 0;
|
|
32384
|
+
const trimmed = raw.trim();
|
|
32385
|
+
if (trimmed === "") return void 0;
|
|
32386
|
+
const value = Number(trimmed);
|
|
32387
|
+
return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
32388
|
+
}
|
|
32389
|
+
function withSpendPolicy(inner, control, consumeEstimateUsd) {
|
|
32390
|
+
return async (input, init2) => {
|
|
32391
|
+
const published = consumeEstimateUsd();
|
|
32392
|
+
if (published === void 0 || !control.hasAmountLimits()) return inner(input, init2);
|
|
32393
|
+
const estimate = Number.isFinite(published) && published > 0 ? published : 0;
|
|
32394
|
+
const result = control.checkAmount(estimate);
|
|
32395
|
+
if (!result.allowed) {
|
|
32396
|
+
throw new SpendPolicyError(result.reason ?? "blocked by spend policy", {
|
|
32397
|
+
blockedBy: result.blockedBy
|
|
32398
|
+
});
|
|
32399
|
+
}
|
|
32400
|
+
const reservationId = control.hasAggregateLimits() ? control.reserve(estimate) : void 0;
|
|
32401
|
+
let response;
|
|
32402
|
+
try {
|
|
32403
|
+
response = await inner(input, init2);
|
|
32404
|
+
} catch (err) {
|
|
32405
|
+
if (reservationId !== void 0) control.releaseReservation(reservationId);
|
|
32406
|
+
throw err;
|
|
32407
|
+
}
|
|
32408
|
+
const settled = settledChargeUsd(response);
|
|
32409
|
+
const charged = response.ok ? settled ?? estimate : settled ?? 0;
|
|
32410
|
+
if (reservationId !== void 0) control.releaseReservation(reservationId);
|
|
32411
|
+
if (charged > 0) control.record(charged, { action: "BlockRun account charge" });
|
|
32412
|
+
return response;
|
|
32413
|
+
};
|
|
32414
|
+
}
|
|
32381
32415
|
function formatDuration(seconds) {
|
|
32382
32416
|
if (seconds < 60) {
|
|
32383
32417
|
return `${seconds}s`;
|
|
@@ -32707,6 +32741,29 @@ var init_spend_control = __esm({
|
|
|
32707
32741
|
};
|
|
32708
32742
|
}
|
|
32709
32743
|
}
|
|
32744
|
+
return this.checkAmount(estimatedCost2);
|
|
32745
|
+
}
|
|
32746
|
+
/**
|
|
32747
|
+
* The amount windows alone, with no counterparty to inspect.
|
|
32748
|
+
*
|
|
32749
|
+
* `perRequest` / `hourly` / `daily` / `session` are denominated in USD and
|
|
32750
|
+
* say nothing about how the money moves, so they are the part of the policy
|
|
32751
|
+
* that means the same thing on the API-key rail as on the wallet rail — a
|
|
32752
|
+
* daily cap is as sensible against account credit as against USDC, and both
|
|
32753
|
+
* are read from the same `spending.json` (#329). The counterparty lists are
|
|
32754
|
+
* the part that does NOT translate: `blockedPayees`, `allowedPayees`,
|
|
32755
|
+
* `blockedNetworks` and `allowedAssets` presuppose a payee, a network and an
|
|
32756
|
+
* asset, and on the key rail there is one counterparty and no on-chain asset.
|
|
32757
|
+
* They are vacuous there rather than missing, which is why `check()` layers
|
|
32758
|
+
* them on top of this instead of the other way round.
|
|
32759
|
+
*/
|
|
32760
|
+
checkAmount(estimatedCost2) {
|
|
32761
|
+
if (this.policyFileBroken !== void 0) {
|
|
32762
|
+
return {
|
|
32763
|
+
allowed: false,
|
|
32764
|
+
reason: `Spend policy is unreadable, refusing all payments: ${this.policyFileBroken}`
|
|
32765
|
+
};
|
|
32766
|
+
}
|
|
32710
32767
|
const now2 = this.now();
|
|
32711
32768
|
if (this.limits.perRequest !== void 0) {
|
|
32712
32769
|
if (estimatedCost2 > this.limits.perRequest) {
|
|
@@ -59882,6 +59939,28 @@ Then run: npx @blockrun/clawrouter`
|
|
|
59882
59939
|
solanaPrivateKeyBytes
|
|
59883
59940
|
};
|
|
59884
59941
|
}
|
|
59942
|
+
async function warnIfLegacyWalletDiffers(coreAddress) {
|
|
59943
|
+
if (legacyWalletDivergenceWarned) return;
|
|
59944
|
+
let legacyAddress;
|
|
59945
|
+
try {
|
|
59946
|
+
const saved = await loadSavedWallet();
|
|
59947
|
+
if (!saved) return;
|
|
59948
|
+
legacyAddress = privateKeyToAccount(saved).address;
|
|
59949
|
+
} catch {
|
|
59950
|
+
return;
|
|
59951
|
+
}
|
|
59952
|
+
if (legacyAddress === coreAddress) return;
|
|
59953
|
+
legacyWalletDivergenceWarned = true;
|
|
59954
|
+
console.warn(
|
|
59955
|
+
`[ClawRouter] \u26A0 Two different wallets are configured. Paying from the BlockRun Core wallet ${coreAddress}.`
|
|
59956
|
+
);
|
|
59957
|
+
console.warn(
|
|
59958
|
+
`[ClawRouter] ClawRouter's older wallet ${legacyAddress} (${WALLET_FILE}) is NOT being used \u2014 if that is the funded one, requests will fail on an empty balance.`
|
|
59959
|
+
);
|
|
59960
|
+
console.warn(
|
|
59961
|
+
`[ClawRouter] To keep paying from it: export BLOCKRUN_WALLET_KEY=$(cat ${WALLET_FILE})`
|
|
59962
|
+
);
|
|
59963
|
+
}
|
|
59885
59964
|
async function resolveExistingWalletKey() {
|
|
59886
59965
|
const coreSolanaKey = await loadCoreSolanaKeyForSelectedChain();
|
|
59887
59966
|
const envKey = process["env"].BLOCKRUN_WALLET_KEY;
|
|
@@ -59897,6 +59976,7 @@ async function resolveExistingWalletKey() {
|
|
|
59897
59976
|
const core = await loadCoreWallet();
|
|
59898
59977
|
if (core) {
|
|
59899
59978
|
const account = privateKeyToAccount(core);
|
|
59979
|
+
await warnIfLegacyWalletDiffers(account.address);
|
|
59900
59980
|
return {
|
|
59901
59981
|
key: core,
|
|
59902
59982
|
address: account.address,
|
|
@@ -60064,13 +60144,28 @@ async function savePaymentChain(chain3) {
|
|
|
60064
60144
|
}
|
|
60065
60145
|
async function loadPaymentChain() {
|
|
60066
60146
|
const core = await readOptional(CORE_CHAIN_FILE);
|
|
60067
|
-
if (core === "solana") return "solana";
|
|
60068
|
-
if (core === "base") return "base";
|
|
60069
60147
|
const legacy = await readOptional(CHAIN_FILE);
|
|
60148
|
+
if (core === "solana" || core === "base") {
|
|
60149
|
+
if ((legacy === "solana" || legacy === "base") && legacy !== core) {
|
|
60150
|
+
warnChainOverride(core, legacy);
|
|
60151
|
+
}
|
|
60152
|
+
return core;
|
|
60153
|
+
}
|
|
60070
60154
|
if (legacy === "solana") return "solana";
|
|
60071
60155
|
if (legacy === "base") return "base";
|
|
60072
60156
|
return await hasExistingWallet() ? "base" : "solana";
|
|
60073
60157
|
}
|
|
60158
|
+
function warnChainOverride(core, legacy) {
|
|
60159
|
+
if (chainOverrideWarned) return;
|
|
60160
|
+
chainOverrideWarned = true;
|
|
60161
|
+
console.warn(
|
|
60162
|
+
`[ClawRouter] \u26A0 Payment chain is ${core} (from ${CORE_CHAIN_FILE}), overriding this install's saved ${legacy}.`
|
|
60163
|
+
);
|
|
60164
|
+
console.warn(
|
|
60165
|
+
`[ClawRouter] A different chain means a different signer and gateway \u2014 funds on ${legacy} will not be spendable.`
|
|
60166
|
+
);
|
|
60167
|
+
console.warn(`[ClawRouter] To go back: npx @blockrun/clawrouter wallet ${legacy}`);
|
|
60168
|
+
}
|
|
60074
60169
|
async function hasExistingWallet() {
|
|
60075
60170
|
if (process["env"].BLOCKRUN_WALLET_KEY?.trim()) return true;
|
|
60076
60171
|
const [core, legacy] = await Promise.all([
|
|
@@ -60084,7 +60179,7 @@ async function resolvePaymentChain() {
|
|
|
60084
60179
|
if (process["env"].CLAWROUTER_PAYMENT_CHAIN === "base") return "base";
|
|
60085
60180
|
return loadPaymentChain();
|
|
60086
60181
|
}
|
|
60087
|
-
var WALLET_DIR2, WALLET_FILE, MNEMONIC_FILE, CHAIN_FILE, CORE_WALLET_DIR, CORE_WALLET_FILE, CORE_SOLANA_WALLET_FILE, CORE_CHAIN_FILE;
|
|
60182
|
+
var WALLET_DIR2, WALLET_FILE, MNEMONIC_FILE, CHAIN_FILE, CORE_WALLET_DIR, CORE_WALLET_FILE, CORE_SOLANA_WALLET_FILE, CORE_CHAIN_FILE, legacyWalletDivergenceWarned, chainOverrideWarned;
|
|
60088
60183
|
var init_auth = __esm({
|
|
60089
60184
|
"src/auth.ts"() {
|
|
60090
60185
|
"use strict";
|
|
@@ -60100,6 +60195,8 @@ var init_auth = __esm({
|
|
|
60100
60195
|
CORE_WALLET_FILE = join7(CORE_WALLET_DIR, ".session");
|
|
60101
60196
|
CORE_SOLANA_WALLET_FILE = join7(CORE_WALLET_DIR, ".solana-session");
|
|
60102
60197
|
CORE_CHAIN_FILE = join7(CORE_WALLET_DIR, ".chain");
|
|
60198
|
+
legacyWalletDivergenceWarned = false;
|
|
60199
|
+
chainOverrideWarned = false;
|
|
60103
60200
|
}
|
|
60104
60201
|
});
|
|
60105
60202
|
|
|
@@ -91608,6 +91705,10 @@ function settledMediaCostUsd(upstream, body, x402AmountUsd, estimate) {
|
|
|
91608
91705
|
if (typeof x402AmountUsd === "number" && x402AmountUsd > 0) return x402AmountUsd;
|
|
91609
91706
|
return estimate;
|
|
91610
91707
|
}
|
|
91708
|
+
function publishDispatchCost(usd2) {
|
|
91709
|
+
const store = paymentStore.getStore();
|
|
91710
|
+
if (store) store.estimatedUsd = Number.isFinite(usd2) && usd2 > 0 ? usd2 : 0;
|
|
91711
|
+
}
|
|
91611
91712
|
function noteRemainingCredit(response) {
|
|
91612
91713
|
const remaining = gatewayRemainingCreditUsd(response);
|
|
91613
91714
|
if (remaining === void 0) return;
|
|
@@ -91657,6 +91758,7 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
|
|
|
91657
91758
|
res.on("close", () => {
|
|
91658
91759
|
if (!res.writableEnded) clientAbort.abort();
|
|
91659
91760
|
});
|
|
91761
|
+
publishDispatchCost(0);
|
|
91660
91762
|
const upstream = await payFetch(upstreamUrl, {
|
|
91661
91763
|
method: req.method ?? "POST",
|
|
91662
91764
|
headers,
|
|
@@ -91766,18 +91868,49 @@ function warnIfSpendLimitsUnenforced(spendControl) {
|
|
|
91766
91868
|
let configured;
|
|
91767
91869
|
try {
|
|
91768
91870
|
const limits = (spendControl ?? new SpendControl()).getStatus().limits;
|
|
91769
|
-
configured =
|
|
91871
|
+
configured = POLICY_LISTS.filter((list) => (limits[list]?.length ?? 0) > 0);
|
|
91770
91872
|
} catch {
|
|
91771
91873
|
return;
|
|
91772
91874
|
}
|
|
91773
91875
|
if (configured.length === 0) return;
|
|
91774
91876
|
console.warn(
|
|
91775
|
-
`[ClawRouter] \u26A0
|
|
91877
|
+
`[ClawRouter] \u26A0 Counterparty policy (${configured.join(", ")}) does not apply in API-key mode \u2014 there is one counterparty and no on-chain asset.`
|
|
91776
91878
|
);
|
|
91777
91879
|
console.warn(
|
|
91778
|
-
`[ClawRouter] Your
|
|
91880
|
+
`[ClawRouter] Your perRequest/hourly/daily/session limits DO apply here and are enforced per call.`
|
|
91779
91881
|
);
|
|
91780
91882
|
}
|
|
91883
|
+
function assertExistingProxyBillsUs(existing, requested) {
|
|
91884
|
+
const { listenPort } = requested;
|
|
91885
|
+
const describe = (mode) => mode === "api-key" ? "a BlockRun API key" : "a wallet";
|
|
91886
|
+
if (existing.authMode !== requested.authMode) {
|
|
91887
|
+
throw new Error(
|
|
91888
|
+
`Existing proxy on port ${listenPort} is authenticating with ${describe(existing.authMode)} but ${describe(requested.authMode)} was requested. Stop the existing proxy first or use a different port.`
|
|
91889
|
+
);
|
|
91890
|
+
}
|
|
91891
|
+
if (requested.authMode === "api-key") {
|
|
91892
|
+
if (existing.apiKeyLabel !== requested.apiKeyLabel) {
|
|
91893
|
+
throw new Error(
|
|
91894
|
+
`Existing proxy on port ${listenPort} is billing ${existing.apiKeyLabel ?? "an unidentified BlockRun account"}, but this process is configured for ${requested.apiKeyLabel}. Reusing it would charge the other account. Stop that proxy first, or use a different port.`
|
|
91895
|
+
);
|
|
91896
|
+
}
|
|
91897
|
+
return;
|
|
91898
|
+
}
|
|
91899
|
+
if (existing.paymentChain) {
|
|
91900
|
+
if (existing.paymentChain !== requested.paymentChain) {
|
|
91901
|
+
throw new Error(
|
|
91902
|
+
`Existing proxy on port ${listenPort} is using ${existing.paymentChain} but ${requested.paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
91903
|
+
);
|
|
91904
|
+
}
|
|
91905
|
+
} else if (requested.paymentChain !== "base") {
|
|
91906
|
+
console.warn(
|
|
91907
|
+
`[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`
|
|
91908
|
+
);
|
|
91909
|
+
throw new Error(
|
|
91910
|
+
`Existing proxy on port ${listenPort} is a pre-v0.11 instance (assumed Base) but ${requested.paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
91911
|
+
);
|
|
91912
|
+
}
|
|
91913
|
+
}
|
|
91781
91914
|
async function startProxy(options) {
|
|
91782
91915
|
const upstreamProxy = await applyUpstreamProxy(options.upstreamProxy);
|
|
91783
91916
|
if (upstreamProxy) {
|
|
@@ -91814,32 +91947,29 @@ async function startProxy(options) {
|
|
|
91814
91947
|
console.warn(`[ClawRouter] or run "npx @blockrun/clawrouter chain base" to switch to EVM.`);
|
|
91815
91948
|
} else if (paymentChain === "solana") {
|
|
91816
91949
|
console.log(`[ClawRouter] Payment chain: Solana (${BLOCKRUN_SOLANA_API})`);
|
|
91950
|
+
} else {
|
|
91951
|
+
console.log(`[ClawRouter] Payment chain: Base (${apiBase})`);
|
|
91817
91952
|
}
|
|
91818
91953
|
void loadGatewayCatalog(apiBase, apiKey);
|
|
91819
91954
|
const listenPort = options.port ?? getProxyPort();
|
|
91820
91955
|
const existingProxy = options.allowExistingProxy === false ? void 0 : await checkExistingProxy(listenPort);
|
|
91956
|
+
const ourApiKeyLabel = apiKey ? maskApiKey(apiKey) : void 0;
|
|
91821
91957
|
if (existingProxy) {
|
|
91822
91958
|
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
91823
|
-
|
|
91824
|
-
|
|
91825
|
-
|
|
91826
|
-
|
|
91827
|
-
|
|
91959
|
+
assertExistingProxyBillsUs(existingProxy, {
|
|
91960
|
+
listenPort,
|
|
91961
|
+
authMode,
|
|
91962
|
+
apiKeyLabel: ourApiKeyLabel,
|
|
91963
|
+
paymentChain
|
|
91964
|
+
});
|
|
91828
91965
|
if (authMode === "api-key") {
|
|
91829
|
-
const runningLabel = existingProxy.apiKeyLabel;
|
|
91830
|
-
const ourLabel = maskApiKey(apiKey);
|
|
91831
|
-
if (runningLabel !== ourLabel) {
|
|
91832
|
-
throw new Error(
|
|
91833
|
-
`Existing proxy on port ${listenPort} is billing ${runningLabel ?? "an unidentified BlockRun account"}, but this process is configured for ${ourLabel}. Reusing it would charge the other account. Stop that proxy first, or use a different port.`
|
|
91834
|
-
);
|
|
91835
|
-
}
|
|
91836
91966
|
options.onReady?.(listenPort);
|
|
91837
91967
|
return {
|
|
91838
91968
|
port: listenPort,
|
|
91839
91969
|
baseUrl: baseUrl2,
|
|
91840
91970
|
walletAddress: "",
|
|
91841
91971
|
authMode,
|
|
91842
|
-
apiKeyLabel:
|
|
91972
|
+
apiKeyLabel: ourApiKeyLabel,
|
|
91843
91973
|
balanceMonitor: new ApiKeyBalanceMonitor(),
|
|
91844
91974
|
// No-op: we didn't start this proxy, so we shouldn't close it
|
|
91845
91975
|
close: async () => {
|
|
@@ -91852,20 +91982,6 @@ async function startProxy(options) {
|
|
|
91852
91982
|
`[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingProxy.wallet}, but current config uses ${account2.address}. Reusing existing proxy.`
|
|
91853
91983
|
);
|
|
91854
91984
|
}
|
|
91855
|
-
if (existingProxy.paymentChain) {
|
|
91856
|
-
if (existingProxy.paymentChain !== paymentChain) {
|
|
91857
|
-
throw new Error(
|
|
91858
|
-
`Existing proxy on port ${listenPort} is using ${existingProxy.paymentChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
91859
|
-
);
|
|
91860
|
-
}
|
|
91861
|
-
} else if (paymentChain !== "base") {
|
|
91862
|
-
console.warn(
|
|
91863
|
-
`[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`
|
|
91864
|
-
);
|
|
91865
|
-
throw new Error(
|
|
91866
|
-
`Existing proxy on port ${listenPort} is a pre-v0.11 instance (assumed Base) but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
91867
|
-
);
|
|
91868
|
-
}
|
|
91869
91985
|
let reuseSolanaAddress;
|
|
91870
91986
|
if (solanaPrivateKeyBytes) {
|
|
91871
91987
|
const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await Promise.resolve().then(() => (init_index_node37(), index_node_exports));
|
|
@@ -91935,7 +92051,16 @@ async function startProxy(options) {
|
|
|
91935
92051
|
if (store) store.amountUsd = amountUsd;
|
|
91936
92052
|
console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
|
|
91937
92053
|
});
|
|
91938
|
-
const payFetch = authMode === "api-key" ?
|
|
92054
|
+
const payFetch = authMode === "api-key" ? withSpendPolicy(
|
|
92055
|
+
createApiKeyFetch(apiKey, fetch, apiBase),
|
|
92056
|
+
options.spendControl ?? getSharedSpendControl(),
|
|
92057
|
+
() => {
|
|
92058
|
+
const store = paymentStore.getStore();
|
|
92059
|
+
const published = store?.estimatedUsd;
|
|
92060
|
+
if (store) store.estimatedUsd = void 0;
|
|
92061
|
+
return published;
|
|
92062
|
+
}
|
|
92063
|
+
) : createPayFetchWithPreAuth(fetch, x402, void 0, {
|
|
91939
92064
|
skipPreAuth: paymentChain === "solana",
|
|
91940
92065
|
// Per-request cost estimate so pre-auth is only reused when the cached
|
|
91941
92066
|
// payment still covers the (possibly larger) request — BlockRun prices per
|
|
@@ -92000,6 +92125,7 @@ async function startProxy(options) {
|
|
|
92000
92125
|
} else {
|
|
92001
92126
|
response.wallet = account.address;
|
|
92002
92127
|
response.paymentChain = paymentChain;
|
|
92128
|
+
response.gateway = apiBase;
|
|
92003
92129
|
if (solanaAddress) {
|
|
92004
92130
|
response.solana = solanaAddress;
|
|
92005
92131
|
}
|
|
@@ -92318,6 +92444,7 @@ async function startProxy(options) {
|
|
|
92318
92444
|
} catch {
|
|
92319
92445
|
}
|
|
92320
92446
|
try {
|
|
92447
|
+
publishDispatchCost(imgCost);
|
|
92321
92448
|
const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
|
|
92322
92449
|
method: "POST",
|
|
92323
92450
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -92516,6 +92643,7 @@ async function startProxy(options) {
|
|
|
92516
92643
|
return;
|
|
92517
92644
|
}
|
|
92518
92645
|
try {
|
|
92646
|
+
publishDispatchCost(img2imgCost);
|
|
92519
92647
|
let upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
|
|
92520
92648
|
method: "POST",
|
|
92521
92649
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -92619,6 +92747,7 @@ async function startProxy(options) {
|
|
|
92619
92747
|
} catch {
|
|
92620
92748
|
}
|
|
92621
92749
|
try {
|
|
92750
|
+
publishDispatchCost(0.15);
|
|
92622
92751
|
let upstream = await payFetch(`${apiBase}/v1/audio/generations`, {
|
|
92623
92752
|
method: "POST",
|
|
92624
92753
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -92718,6 +92847,7 @@ async function startProxy(options) {
|
|
|
92718
92847
|
} catch {
|
|
92719
92848
|
}
|
|
92720
92849
|
try {
|
|
92850
|
+
publishDispatchCost(estimateVideoCost(videoModel, videoDuration, videoHasImageInput));
|
|
92721
92851
|
const submitResp = await payFetch(`${apiBase}/v1/videos/generations`, {
|
|
92722
92852
|
method: "POST",
|
|
92723
92853
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -93002,11 +93132,7 @@ async function startProxy(options) {
|
|
|
93002
93132
|
const existingProxy2 = await checkExistingProxy(listenPort);
|
|
93003
93133
|
if (existingProxy2) {
|
|
93004
93134
|
console.log(`[ClawRouter] Existing proxy detected on port ${listenPort}, reusing`);
|
|
93005
|
-
rejectAttempt({
|
|
93006
|
-
code: "REUSE_EXISTING",
|
|
93007
|
-
wallet: existingProxy2.wallet,
|
|
93008
|
-
existingChain: existingProxy2.paymentChain
|
|
93009
|
-
});
|
|
93135
|
+
rejectAttempt({ code: "REUSE_EXISTING", existing: existingProxy2 });
|
|
93010
93136
|
return;
|
|
93011
93137
|
}
|
|
93012
93138
|
if (attempt < PORT_RETRY_ATTEMPTS) {
|
|
@@ -93038,21 +93164,21 @@ async function startProxy(options) {
|
|
|
93038
93164
|
break;
|
|
93039
93165
|
} catch (err) {
|
|
93040
93166
|
const error = err;
|
|
93041
|
-
if (error.code === "REUSE_EXISTING" && error.
|
|
93042
|
-
|
|
93043
|
-
|
|
93044
|
-
|
|
93045
|
-
|
|
93046
|
-
|
|
93047
|
-
}
|
|
93167
|
+
if (error.code === "REUSE_EXISTING" && error.existing) {
|
|
93168
|
+
assertExistingProxyBillsUs(error.existing, {
|
|
93169
|
+
listenPort,
|
|
93170
|
+
authMode,
|
|
93171
|
+
apiKeyLabel: ourApiKeyLabel,
|
|
93172
|
+
paymentChain
|
|
93173
|
+
});
|
|
93048
93174
|
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
93049
93175
|
options.onReady?.(listenPort);
|
|
93050
93176
|
return {
|
|
93051
93177
|
port: listenPort,
|
|
93052
93178
|
baseUrl: baseUrl2,
|
|
93053
|
-
walletAddress: error.wallet,
|
|
93179
|
+
walletAddress: error.existing.wallet,
|
|
93054
93180
|
authMode,
|
|
93055
|
-
...
|
|
93181
|
+
...ourApiKeyLabel ? { apiKeyLabel: ourApiKeyLabel } : {},
|
|
93056
93182
|
balanceMonitor,
|
|
93057
93183
|
close: async () => {
|
|
93058
93184
|
}
|
|
@@ -93517,6 +93643,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
93517
93643
|
size: imageSize,
|
|
93518
93644
|
n: 1
|
|
93519
93645
|
});
|
|
93646
|
+
publishDispatchCost(estimateImageCost(imageModel, imageSize, 1));
|
|
93520
93647
|
const imageResponse = await payFetch(imageUpstreamUrl, {
|
|
93521
93648
|
method: "POST",
|
|
93522
93649
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -93798,6 +93925,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
93798
93925
|
size: img2imgSize,
|
|
93799
93926
|
n: 1
|
|
93800
93927
|
});
|
|
93928
|
+
publishDispatchCost(estimateImageCost(img2imgModel, img2imgSize, 1));
|
|
93801
93929
|
const img2imgResponse = await payFetch(`${apiBase}/v1/images/image2image`, {
|
|
93802
93930
|
method: "POST",
|
|
93803
93931
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -94627,6 +94755,8 @@ data: [DONE]
|
|
|
94627
94755
|
throw abortError();
|
|
94628
94756
|
}
|
|
94629
94757
|
console.log(`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`);
|
|
94758
|
+
const attemptEst = FREE_MODELS.has(tryModel) ? void 0 : estimateAmount(tryModel, body.length, maxTokens);
|
|
94759
|
+
publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
|
|
94630
94760
|
const perAttemptTimeoutMs = timeoutForModel(tryModel);
|
|
94631
94761
|
const modelController = new AbortController();
|
|
94632
94762
|
const modelTimeoutId = setTimeout(() => modelController.abort(), perAttemptTimeoutMs);
|
|
@@ -94726,6 +94856,7 @@ data: [DONE]
|
|
|
94726
94856
|
);
|
|
94727
94857
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
94728
94858
|
if (!globalController.signal.aborted) {
|
|
94859
|
+
publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
|
|
94729
94860
|
const retryController = new AbortController();
|
|
94730
94861
|
const retryTimeoutId = setTimeout(
|
|
94731
94862
|
() => retryController.abort(),
|
|
@@ -94788,6 +94919,7 @@ data: [DONE]
|
|
|
94788
94919
|
);
|
|
94789
94920
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
94790
94921
|
if (!globalController.signal.aborted) {
|
|
94922
|
+
publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
|
|
94791
94923
|
const retryController = new AbortController();
|
|
94792
94924
|
const retryTimeoutId = setTimeout(
|
|
94793
94925
|
() => retryController.abort(),
|
|
@@ -228566,6 +228698,19 @@ function isBlockrunWebSearchDisabled(config) {
|
|
|
228566
228698
|
const cfg = config ?? {};
|
|
228567
228699
|
return cfg.tools?.web?.search?.enabled === false;
|
|
228568
228700
|
}
|
|
228701
|
+
function legacyPackageIsBlockRun() {
|
|
228702
|
+
for (const dir of [
|
|
228703
|
+
join18(homedir15(), ".openclaw", "extensions", "clawrouter"),
|
|
228704
|
+
join18(homedir15(), ".openclaw", "npm", "node_modules", "@blockrun", "clawrouter")
|
|
228705
|
+
]) {
|
|
228706
|
+
try {
|
|
228707
|
+
const metadata = JSON.parse(readTextFileSync(join18(dir, "package.json")));
|
|
228708
|
+
if (metadata.name === "@blockrun/clawrouter") return true;
|
|
228709
|
+
} catch {
|
|
228710
|
+
}
|
|
228711
|
+
}
|
|
228712
|
+
return false;
|
|
228713
|
+
}
|
|
228569
228714
|
function injectModelsConfig(logger48, options = {}) {
|
|
228570
228715
|
const configDir = join18(homedir15(), ".openclaw");
|
|
228571
228716
|
const configPath = join18(configDir, "openclaw.json");
|
|
@@ -228729,9 +228874,9 @@ function injectModelsConfig(logger48, options = {}) {
|
|
|
228729
228874
|
const legacyEntry = config.plugins?.entries?.clawrouter;
|
|
228730
228875
|
const legacyEntryConfig = legacyEntry?.config;
|
|
228731
228876
|
const legacyBlockRunInstall = Boolean(
|
|
228732
|
-
legacyEntry && ["walletKey", "routing"].some(
|
|
228877
|
+
legacyEntry && (["walletKey", "routing"].some(
|
|
228733
228878
|
(key2) => key2 in legacyEntry || (legacyEntryConfig ? key2 in legacyEntryConfig : false)
|
|
228734
|
-
)
|
|
228879
|
+
) || legacyPackageIsBlockRun())
|
|
228735
228880
|
);
|
|
228736
228881
|
if (legacyBlockRunInstall && prepareBlockRunPluginConfig(config, { legacyBlockRunInstall })) {
|
|
228737
228882
|
needsWrite = true;
|
package/dist/index.d.ts
CHANGED
|
@@ -834,6 +834,21 @@ declare class SpendControl {
|
|
|
834
834
|
*/
|
|
835
835
|
reloadLimits(): void;
|
|
836
836
|
check(estimatedCost: number, counterparty?: CounterpartyInfo): CheckResult;
|
|
837
|
+
/**
|
|
838
|
+
* The amount windows alone, with no counterparty to inspect.
|
|
839
|
+
*
|
|
840
|
+
* `perRequest` / `hourly` / `daily` / `session` are denominated in USD and
|
|
841
|
+
* say nothing about how the money moves, so they are the part of the policy
|
|
842
|
+
* that means the same thing on the API-key rail as on the wallet rail — a
|
|
843
|
+
* daily cap is as sensible against account credit as against USDC, and both
|
|
844
|
+
* are read from the same `spending.json` (#329). The counterparty lists are
|
|
845
|
+
* the part that does NOT translate: `blockedPayees`, `allowedPayees`,
|
|
846
|
+
* `blockedNetworks` and `allowedAssets` presuppose a payee, a network and an
|
|
847
|
+
* asset, and on the key rail there is one counterparty and no on-chain asset.
|
|
848
|
+
* They are vacuous there rather than missing, which is why `check()` layers
|
|
849
|
+
* them on top of this instead of the other way round.
|
|
850
|
+
*/
|
|
851
|
+
checkAmount(estimatedCost: number): CheckResult;
|
|
837
852
|
record(amount: number, metadata?: {
|
|
838
853
|
model?: string;
|
|
839
854
|
action?: string;
|
|
@@ -1219,6 +1234,14 @@ type ProxyHandle = {
|
|
|
1219
1234
|
balanceMonitor: AnyBalanceMonitor;
|
|
1220
1235
|
close: () => Promise<void>;
|
|
1221
1236
|
};
|
|
1237
|
+
/**
|
|
1238
|
+
* Start the local x402 proxy server.
|
|
1239
|
+
*
|
|
1240
|
+
* If a proxy is already running on the target port, reuses it instead of failing.
|
|
1241
|
+
* Port can be configured via BLOCKRUN_PROXY_PORT environment variable.
|
|
1242
|
+
*
|
|
1243
|
+
* Returns a handle with the assigned port, base URL, and a close function.
|
|
1244
|
+
*/
|
|
1222
1245
|
declare function startProxy(options: ProxyOptions): Promise<ProxyHandle>;
|
|
1223
1246
|
|
|
1224
1247
|
/**
|
|
@@ -1778,23 +1801,6 @@ declare function buildPartnerTools(proxyBaseUrl: string): PartnerToolDefinition[
|
|
|
1778
1801
|
* opt-in.
|
|
1779
1802
|
*/
|
|
1780
1803
|
declare function isBlockrunWebSearchDisabled(config?: unknown): boolean;
|
|
1781
|
-
/**
|
|
1782
|
-
* Inject BlockRun models config into OpenClaw config file.
|
|
1783
|
-
* This is required because registerProvider() alone doesn't make models available.
|
|
1784
|
-
*
|
|
1785
|
-
* CRITICAL: This function must be idempotent and handle ALL edge cases:
|
|
1786
|
-
* - Config file doesn't exist (create it)
|
|
1787
|
-
* - Config file exists but is empty/invalid (reinitialize)
|
|
1788
|
-
* - blockrun provider exists but has undefined fields (fix them)
|
|
1789
|
-
* - Config exists but uses old port/models (update them)
|
|
1790
|
-
*
|
|
1791
|
-
* This function is called on EVERY plugin load to ensure config is always correct.
|
|
1792
|
-
*
|
|
1793
|
-
* Also strips any previously managed `mcp.servers.blockrun` entry we wrote in
|
|
1794
|
-
* older releases — ClawRouter no longer bundles the MCP bridge (the npx-spawned
|
|
1795
|
-
* grandchildren were leaking). The scrub only removes entries matching the
|
|
1796
|
-
* managed shape; user-defined `blockrun` MCP servers are left alone.
|
|
1797
|
-
*/
|
|
1798
1804
|
declare function injectModelsConfig(logger: {
|
|
1799
1805
|
info: (msg: string) => void;
|
|
1800
1806
|
}, options?: {
|
package/dist/index.js
CHANGED
|
@@ -34514,6 +34514,40 @@ function registerSpendPolicyHook(x402, control) {
|
|
|
34514
34514
|
}
|
|
34515
34515
|
});
|
|
34516
34516
|
}
|
|
34517
|
+
function settledChargeUsd(response) {
|
|
34518
|
+
const raw = response.headers.get("x-blockrun-cost-usd");
|
|
34519
|
+
if (raw === null) return void 0;
|
|
34520
|
+
const trimmed = raw.trim();
|
|
34521
|
+
if (trimmed === "") return void 0;
|
|
34522
|
+
const value = Number(trimmed);
|
|
34523
|
+
return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
34524
|
+
}
|
|
34525
|
+
function withSpendPolicy(inner, control, consumeEstimateUsd) {
|
|
34526
|
+
return async (input, init2) => {
|
|
34527
|
+
const published = consumeEstimateUsd();
|
|
34528
|
+
if (published === void 0 || !control.hasAmountLimits()) return inner(input, init2);
|
|
34529
|
+
const estimate = Number.isFinite(published) && published > 0 ? published : 0;
|
|
34530
|
+
const result = control.checkAmount(estimate);
|
|
34531
|
+
if (!result.allowed) {
|
|
34532
|
+
throw new SpendPolicyError(result.reason ?? "blocked by spend policy", {
|
|
34533
|
+
blockedBy: result.blockedBy
|
|
34534
|
+
});
|
|
34535
|
+
}
|
|
34536
|
+
const reservationId = control.hasAggregateLimits() ? control.reserve(estimate) : void 0;
|
|
34537
|
+
let response;
|
|
34538
|
+
try {
|
|
34539
|
+
response = await inner(input, init2);
|
|
34540
|
+
} catch (err) {
|
|
34541
|
+
if (reservationId !== void 0) control.releaseReservation(reservationId);
|
|
34542
|
+
throw err;
|
|
34543
|
+
}
|
|
34544
|
+
const settled = settledChargeUsd(response);
|
|
34545
|
+
const charged = response.ok ? settled ?? estimate : settled ?? 0;
|
|
34546
|
+
if (reservationId !== void 0) control.releaseReservation(reservationId);
|
|
34547
|
+
if (charged > 0) control.record(charged, { action: "BlockRun account charge" });
|
|
34548
|
+
return response;
|
|
34549
|
+
};
|
|
34550
|
+
}
|
|
34517
34551
|
function formatDuration(seconds) {
|
|
34518
34552
|
if (seconds < 60) {
|
|
34519
34553
|
return `${seconds}s`;
|
|
@@ -34843,6 +34877,29 @@ var init_spend_control = __esm({
|
|
|
34843
34877
|
};
|
|
34844
34878
|
}
|
|
34845
34879
|
}
|
|
34880
|
+
return this.checkAmount(estimatedCost2);
|
|
34881
|
+
}
|
|
34882
|
+
/**
|
|
34883
|
+
* The amount windows alone, with no counterparty to inspect.
|
|
34884
|
+
*
|
|
34885
|
+
* `perRequest` / `hourly` / `daily` / `session` are denominated in USD and
|
|
34886
|
+
* say nothing about how the money moves, so they are the part of the policy
|
|
34887
|
+
* that means the same thing on the API-key rail as on the wallet rail — a
|
|
34888
|
+
* daily cap is as sensible against account credit as against USDC, and both
|
|
34889
|
+
* are read from the same `spending.json` (#329). The counterparty lists are
|
|
34890
|
+
* the part that does NOT translate: `blockedPayees`, `allowedPayees`,
|
|
34891
|
+
* `blockedNetworks` and `allowedAssets` presuppose a payee, a network and an
|
|
34892
|
+
* asset, and on the key rail there is one counterparty and no on-chain asset.
|
|
34893
|
+
* They are vacuous there rather than missing, which is why `check()` layers
|
|
34894
|
+
* them on top of this instead of the other way round.
|
|
34895
|
+
*/
|
|
34896
|
+
checkAmount(estimatedCost2) {
|
|
34897
|
+
if (this.policyFileBroken !== void 0) {
|
|
34898
|
+
return {
|
|
34899
|
+
allowed: false,
|
|
34900
|
+
reason: `Spend policy is unreadable, refusing all payments: ${this.policyFileBroken}`
|
|
34901
|
+
};
|
|
34902
|
+
}
|
|
34846
34903
|
const now2 = this.now();
|
|
34847
34904
|
if (this.limits.perRequest !== void 0) {
|
|
34848
34905
|
if (estimatedCost2 > this.limits.perRequest) {
|
|
@@ -59733,6 +59790,28 @@ Then run: npx @blockrun/clawrouter`
|
|
|
59733
59790
|
solanaPrivateKeyBytes
|
|
59734
59791
|
};
|
|
59735
59792
|
}
|
|
59793
|
+
async function warnIfLegacyWalletDiffers(coreAddress) {
|
|
59794
|
+
if (legacyWalletDivergenceWarned) return;
|
|
59795
|
+
let legacyAddress;
|
|
59796
|
+
try {
|
|
59797
|
+
const saved = await loadSavedWallet();
|
|
59798
|
+
if (!saved) return;
|
|
59799
|
+
legacyAddress = privateKeyToAccount(saved).address;
|
|
59800
|
+
} catch {
|
|
59801
|
+
return;
|
|
59802
|
+
}
|
|
59803
|
+
if (legacyAddress === coreAddress) return;
|
|
59804
|
+
legacyWalletDivergenceWarned = true;
|
|
59805
|
+
console.warn(
|
|
59806
|
+
`[ClawRouter] \u26A0 Two different wallets are configured. Paying from the BlockRun Core wallet ${coreAddress}.`
|
|
59807
|
+
);
|
|
59808
|
+
console.warn(
|
|
59809
|
+
`[ClawRouter] ClawRouter's older wallet ${legacyAddress} (${WALLET_FILE}) is NOT being used \u2014 if that is the funded one, requests will fail on an empty balance.`
|
|
59810
|
+
);
|
|
59811
|
+
console.warn(
|
|
59812
|
+
`[ClawRouter] To keep paying from it: export BLOCKRUN_WALLET_KEY=$(cat ${WALLET_FILE})`
|
|
59813
|
+
);
|
|
59814
|
+
}
|
|
59736
59815
|
async function resolveExistingWalletKey() {
|
|
59737
59816
|
const coreSolanaKey = await loadCoreSolanaKeyForSelectedChain();
|
|
59738
59817
|
const envKey = process["env"].BLOCKRUN_WALLET_KEY;
|
|
@@ -59748,6 +59827,7 @@ async function resolveExistingWalletKey() {
|
|
|
59748
59827
|
const core = await loadCoreWallet();
|
|
59749
59828
|
if (core) {
|
|
59750
59829
|
const account = privateKeyToAccount(core);
|
|
59830
|
+
await warnIfLegacyWalletDiffers(account.address);
|
|
59751
59831
|
return {
|
|
59752
59832
|
key: core,
|
|
59753
59833
|
address: account.address,
|
|
@@ -59871,13 +59951,28 @@ async function savePaymentChain(chain3) {
|
|
|
59871
59951
|
}
|
|
59872
59952
|
async function loadPaymentChain() {
|
|
59873
59953
|
const core = await readOptional(CORE_CHAIN_FILE);
|
|
59874
|
-
if (core === "solana") return "solana";
|
|
59875
|
-
if (core === "base") return "base";
|
|
59876
59954
|
const legacy = await readOptional(CHAIN_FILE);
|
|
59955
|
+
if (core === "solana" || core === "base") {
|
|
59956
|
+
if ((legacy === "solana" || legacy === "base") && legacy !== core) {
|
|
59957
|
+
warnChainOverride(core, legacy);
|
|
59958
|
+
}
|
|
59959
|
+
return core;
|
|
59960
|
+
}
|
|
59877
59961
|
if (legacy === "solana") return "solana";
|
|
59878
59962
|
if (legacy === "base") return "base";
|
|
59879
59963
|
return await hasExistingWallet() ? "base" : "solana";
|
|
59880
59964
|
}
|
|
59965
|
+
function warnChainOverride(core, legacy) {
|
|
59966
|
+
if (chainOverrideWarned) return;
|
|
59967
|
+
chainOverrideWarned = true;
|
|
59968
|
+
console.warn(
|
|
59969
|
+
`[ClawRouter] \u26A0 Payment chain is ${core} (from ${CORE_CHAIN_FILE}), overriding this install's saved ${legacy}.`
|
|
59970
|
+
);
|
|
59971
|
+
console.warn(
|
|
59972
|
+
`[ClawRouter] A different chain means a different signer and gateway \u2014 funds on ${legacy} will not be spendable.`
|
|
59973
|
+
);
|
|
59974
|
+
console.warn(`[ClawRouter] To go back: npx @blockrun/clawrouter wallet ${legacy}`);
|
|
59975
|
+
}
|
|
59881
59976
|
async function hasExistingWallet() {
|
|
59882
59977
|
if (process["env"].BLOCKRUN_WALLET_KEY?.trim()) return true;
|
|
59883
59978
|
const [core, legacy] = await Promise.all([
|
|
@@ -59891,7 +59986,7 @@ async function resolvePaymentChain() {
|
|
|
59891
59986
|
if (process["env"].CLAWROUTER_PAYMENT_CHAIN === "base") return "base";
|
|
59892
59987
|
return loadPaymentChain();
|
|
59893
59988
|
}
|
|
59894
|
-
var WALLET_DIR2, WALLET_FILE, MNEMONIC_FILE, CHAIN_FILE, CORE_WALLET_DIR, CORE_WALLET_FILE, CORE_SOLANA_WALLET_FILE, CORE_CHAIN_FILE;
|
|
59989
|
+
var WALLET_DIR2, WALLET_FILE, MNEMONIC_FILE, CHAIN_FILE, CORE_WALLET_DIR, CORE_WALLET_FILE, CORE_SOLANA_WALLET_FILE, CORE_CHAIN_FILE, legacyWalletDivergenceWarned, chainOverrideWarned;
|
|
59895
59990
|
var init_auth = __esm({
|
|
59896
59991
|
"src/auth.ts"() {
|
|
59897
59992
|
"use strict";
|
|
@@ -59907,6 +60002,8 @@ var init_auth = __esm({
|
|
|
59907
60002
|
CORE_WALLET_FILE = join7(CORE_WALLET_DIR, ".session");
|
|
59908
60003
|
CORE_SOLANA_WALLET_FILE = join7(CORE_WALLET_DIR, ".solana-session");
|
|
59909
60004
|
CORE_CHAIN_FILE = join7(CORE_WALLET_DIR, ".chain");
|
|
60005
|
+
legacyWalletDivergenceWarned = false;
|
|
60006
|
+
chainOverrideWarned = false;
|
|
59910
60007
|
}
|
|
59911
60008
|
});
|
|
59912
60009
|
|
|
@@ -91361,6 +91458,10 @@ function settledMediaCostUsd(upstream, body, x402AmountUsd, estimate) {
|
|
|
91361
91458
|
if (typeof x402AmountUsd === "number" && x402AmountUsd > 0) return x402AmountUsd;
|
|
91362
91459
|
return estimate;
|
|
91363
91460
|
}
|
|
91461
|
+
function publishDispatchCost(usd) {
|
|
91462
|
+
const store = paymentStore.getStore();
|
|
91463
|
+
if (store) store.estimatedUsd = Number.isFinite(usd) && usd > 0 ? usd : 0;
|
|
91464
|
+
}
|
|
91364
91465
|
function noteRemainingCredit(response) {
|
|
91365
91466
|
const remaining = gatewayRemainingCreditUsd(response);
|
|
91366
91467
|
if (remaining === void 0) return;
|
|
@@ -91410,6 +91511,7 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
|
|
|
91410
91511
|
res.on("close", () => {
|
|
91411
91512
|
if (!res.writableEnded) clientAbort.abort();
|
|
91412
91513
|
});
|
|
91514
|
+
publishDispatchCost(0);
|
|
91413
91515
|
const upstream = await payFetch(upstreamUrl, {
|
|
91414
91516
|
method: req.method ?? "POST",
|
|
91415
91517
|
headers,
|
|
@@ -91519,18 +91621,49 @@ function warnIfSpendLimitsUnenforced(spendControl) {
|
|
|
91519
91621
|
let configured;
|
|
91520
91622
|
try {
|
|
91521
91623
|
const limits = (spendControl ?? new SpendControl()).getStatus().limits;
|
|
91522
|
-
configured =
|
|
91624
|
+
configured = POLICY_LISTS.filter((list) => (limits[list]?.length ?? 0) > 0);
|
|
91523
91625
|
} catch {
|
|
91524
91626
|
return;
|
|
91525
91627
|
}
|
|
91526
91628
|
if (configured.length === 0) return;
|
|
91527
91629
|
console.warn(
|
|
91528
|
-
`[ClawRouter] \u26A0
|
|
91630
|
+
`[ClawRouter] \u26A0 Counterparty policy (${configured.join(", ")}) does not apply in API-key mode \u2014 there is one counterparty and no on-chain asset.`
|
|
91529
91631
|
);
|
|
91530
91632
|
console.warn(
|
|
91531
|
-
`[ClawRouter] Your
|
|
91633
|
+
`[ClawRouter] Your perRequest/hourly/daily/session limits DO apply here and are enforced per call.`
|
|
91532
91634
|
);
|
|
91533
91635
|
}
|
|
91636
|
+
function assertExistingProxyBillsUs(existing, requested) {
|
|
91637
|
+
const { listenPort } = requested;
|
|
91638
|
+
const describe = (mode) => mode === "api-key" ? "a BlockRun API key" : "a wallet";
|
|
91639
|
+
if (existing.authMode !== requested.authMode) {
|
|
91640
|
+
throw new Error(
|
|
91641
|
+
`Existing proxy on port ${listenPort} is authenticating with ${describe(existing.authMode)} but ${describe(requested.authMode)} was requested. Stop the existing proxy first or use a different port.`
|
|
91642
|
+
);
|
|
91643
|
+
}
|
|
91644
|
+
if (requested.authMode === "api-key") {
|
|
91645
|
+
if (existing.apiKeyLabel !== requested.apiKeyLabel) {
|
|
91646
|
+
throw new Error(
|
|
91647
|
+
`Existing proxy on port ${listenPort} is billing ${existing.apiKeyLabel ?? "an unidentified BlockRun account"}, but this process is configured for ${requested.apiKeyLabel}. Reusing it would charge the other account. Stop that proxy first, or use a different port.`
|
|
91648
|
+
);
|
|
91649
|
+
}
|
|
91650
|
+
return;
|
|
91651
|
+
}
|
|
91652
|
+
if (existing.paymentChain) {
|
|
91653
|
+
if (existing.paymentChain !== requested.paymentChain) {
|
|
91654
|
+
throw new Error(
|
|
91655
|
+
`Existing proxy on port ${listenPort} is using ${existing.paymentChain} but ${requested.paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
91656
|
+
);
|
|
91657
|
+
}
|
|
91658
|
+
} else if (requested.paymentChain !== "base") {
|
|
91659
|
+
console.warn(
|
|
91660
|
+
`[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`
|
|
91661
|
+
);
|
|
91662
|
+
throw new Error(
|
|
91663
|
+
`Existing proxy on port ${listenPort} is a pre-v0.11 instance (assumed Base) but ${requested.paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
91664
|
+
);
|
|
91665
|
+
}
|
|
91666
|
+
}
|
|
91534
91667
|
async function startProxy(options) {
|
|
91535
91668
|
const upstreamProxy = await applyUpstreamProxy(options.upstreamProxy);
|
|
91536
91669
|
if (upstreamProxy) {
|
|
@@ -91567,32 +91700,29 @@ async function startProxy(options) {
|
|
|
91567
91700
|
console.warn(`[ClawRouter] or run "npx @blockrun/clawrouter chain base" to switch to EVM.`);
|
|
91568
91701
|
} else if (paymentChain === "solana") {
|
|
91569
91702
|
console.log(`[ClawRouter] Payment chain: Solana (${BLOCKRUN_SOLANA_API})`);
|
|
91703
|
+
} else {
|
|
91704
|
+
console.log(`[ClawRouter] Payment chain: Base (${apiBase})`);
|
|
91570
91705
|
}
|
|
91571
91706
|
void loadGatewayCatalog(apiBase, apiKey);
|
|
91572
91707
|
const listenPort = options.port ?? getProxyPort();
|
|
91573
91708
|
const existingProxy = options.allowExistingProxy === false ? void 0 : await checkExistingProxy(listenPort);
|
|
91709
|
+
const ourApiKeyLabel = apiKey ? maskApiKey(apiKey) : void 0;
|
|
91574
91710
|
if (existingProxy) {
|
|
91575
91711
|
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
91576
|
-
|
|
91577
|
-
|
|
91578
|
-
|
|
91579
|
-
|
|
91580
|
-
|
|
91712
|
+
assertExistingProxyBillsUs(existingProxy, {
|
|
91713
|
+
listenPort,
|
|
91714
|
+
authMode,
|
|
91715
|
+
apiKeyLabel: ourApiKeyLabel,
|
|
91716
|
+
paymentChain
|
|
91717
|
+
});
|
|
91581
91718
|
if (authMode === "api-key") {
|
|
91582
|
-
const runningLabel = existingProxy.apiKeyLabel;
|
|
91583
|
-
const ourLabel = maskApiKey(apiKey);
|
|
91584
|
-
if (runningLabel !== ourLabel) {
|
|
91585
|
-
throw new Error(
|
|
91586
|
-
`Existing proxy on port ${listenPort} is billing ${runningLabel ?? "an unidentified BlockRun account"}, but this process is configured for ${ourLabel}. Reusing it would charge the other account. Stop that proxy first, or use a different port.`
|
|
91587
|
-
);
|
|
91588
|
-
}
|
|
91589
91719
|
options.onReady?.(listenPort);
|
|
91590
91720
|
return {
|
|
91591
91721
|
port: listenPort,
|
|
91592
91722
|
baseUrl: baseUrl2,
|
|
91593
91723
|
walletAddress: "",
|
|
91594
91724
|
authMode,
|
|
91595
|
-
apiKeyLabel:
|
|
91725
|
+
apiKeyLabel: ourApiKeyLabel,
|
|
91596
91726
|
balanceMonitor: new ApiKeyBalanceMonitor(),
|
|
91597
91727
|
// No-op: we didn't start this proxy, so we shouldn't close it
|
|
91598
91728
|
close: async () => {
|
|
@@ -91605,20 +91735,6 @@ async function startProxy(options) {
|
|
|
91605
91735
|
`[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingProxy.wallet}, but current config uses ${account2.address}. Reusing existing proxy.`
|
|
91606
91736
|
);
|
|
91607
91737
|
}
|
|
91608
|
-
if (existingProxy.paymentChain) {
|
|
91609
|
-
if (existingProxy.paymentChain !== paymentChain) {
|
|
91610
|
-
throw new Error(
|
|
91611
|
-
`Existing proxy on port ${listenPort} is using ${existingProxy.paymentChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
91612
|
-
);
|
|
91613
|
-
}
|
|
91614
|
-
} else if (paymentChain !== "base") {
|
|
91615
|
-
console.warn(
|
|
91616
|
-
`[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`
|
|
91617
|
-
);
|
|
91618
|
-
throw new Error(
|
|
91619
|
-
`Existing proxy on port ${listenPort} is a pre-v0.11 instance (assumed Base) but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
91620
|
-
);
|
|
91621
|
-
}
|
|
91622
91738
|
let reuseSolanaAddress;
|
|
91623
91739
|
if (solanaPrivateKeyBytes) {
|
|
91624
91740
|
const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await Promise.resolve().then(() => (init_index_node37(), index_node_exports));
|
|
@@ -91688,7 +91804,16 @@ async function startProxy(options) {
|
|
|
91688
91804
|
if (store) store.amountUsd = amountUsd;
|
|
91689
91805
|
console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
|
|
91690
91806
|
});
|
|
91691
|
-
const payFetch = authMode === "api-key" ?
|
|
91807
|
+
const payFetch = authMode === "api-key" ? withSpendPolicy(
|
|
91808
|
+
createApiKeyFetch(apiKey, fetch, apiBase),
|
|
91809
|
+
options.spendControl ?? getSharedSpendControl(),
|
|
91810
|
+
() => {
|
|
91811
|
+
const store = paymentStore.getStore();
|
|
91812
|
+
const published = store?.estimatedUsd;
|
|
91813
|
+
if (store) store.estimatedUsd = void 0;
|
|
91814
|
+
return published;
|
|
91815
|
+
}
|
|
91816
|
+
) : createPayFetchWithPreAuth(fetch, x402, void 0, {
|
|
91692
91817
|
skipPreAuth: paymentChain === "solana",
|
|
91693
91818
|
// Per-request cost estimate so pre-auth is only reused when the cached
|
|
91694
91819
|
// payment still covers the (possibly larger) request — BlockRun prices per
|
|
@@ -91753,6 +91878,7 @@ async function startProxy(options) {
|
|
|
91753
91878
|
} else {
|
|
91754
91879
|
response.wallet = account.address;
|
|
91755
91880
|
response.paymentChain = paymentChain;
|
|
91881
|
+
response.gateway = apiBase;
|
|
91756
91882
|
if (solanaAddress) {
|
|
91757
91883
|
response.solana = solanaAddress;
|
|
91758
91884
|
}
|
|
@@ -92071,6 +92197,7 @@ async function startProxy(options) {
|
|
|
92071
92197
|
} catch {
|
|
92072
92198
|
}
|
|
92073
92199
|
try {
|
|
92200
|
+
publishDispatchCost(imgCost);
|
|
92074
92201
|
const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
|
|
92075
92202
|
method: "POST",
|
|
92076
92203
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -92269,6 +92396,7 @@ async function startProxy(options) {
|
|
|
92269
92396
|
return;
|
|
92270
92397
|
}
|
|
92271
92398
|
try {
|
|
92399
|
+
publishDispatchCost(img2imgCost);
|
|
92272
92400
|
let upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
|
|
92273
92401
|
method: "POST",
|
|
92274
92402
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -92372,6 +92500,7 @@ async function startProxy(options) {
|
|
|
92372
92500
|
} catch {
|
|
92373
92501
|
}
|
|
92374
92502
|
try {
|
|
92503
|
+
publishDispatchCost(0.15);
|
|
92375
92504
|
let upstream = await payFetch(`${apiBase}/v1/audio/generations`, {
|
|
92376
92505
|
method: "POST",
|
|
92377
92506
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -92471,6 +92600,7 @@ async function startProxy(options) {
|
|
|
92471
92600
|
} catch {
|
|
92472
92601
|
}
|
|
92473
92602
|
try {
|
|
92603
|
+
publishDispatchCost(estimateVideoCost(videoModel, videoDuration, videoHasImageInput));
|
|
92474
92604
|
const submitResp = await payFetch(`${apiBase}/v1/videos/generations`, {
|
|
92475
92605
|
method: "POST",
|
|
92476
92606
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -92755,11 +92885,7 @@ async function startProxy(options) {
|
|
|
92755
92885
|
const existingProxy2 = await checkExistingProxy(listenPort);
|
|
92756
92886
|
if (existingProxy2) {
|
|
92757
92887
|
console.log(`[ClawRouter] Existing proxy detected on port ${listenPort}, reusing`);
|
|
92758
|
-
rejectAttempt({
|
|
92759
|
-
code: "REUSE_EXISTING",
|
|
92760
|
-
wallet: existingProxy2.wallet,
|
|
92761
|
-
existingChain: existingProxy2.paymentChain
|
|
92762
|
-
});
|
|
92888
|
+
rejectAttempt({ code: "REUSE_EXISTING", existing: existingProxy2 });
|
|
92763
92889
|
return;
|
|
92764
92890
|
}
|
|
92765
92891
|
if (attempt < PORT_RETRY_ATTEMPTS) {
|
|
@@ -92791,21 +92917,21 @@ async function startProxy(options) {
|
|
|
92791
92917
|
break;
|
|
92792
92918
|
} catch (err) {
|
|
92793
92919
|
const error = err;
|
|
92794
|
-
if (error.code === "REUSE_EXISTING" && error.
|
|
92795
|
-
|
|
92796
|
-
|
|
92797
|
-
|
|
92798
|
-
|
|
92799
|
-
|
|
92800
|
-
}
|
|
92920
|
+
if (error.code === "REUSE_EXISTING" && error.existing) {
|
|
92921
|
+
assertExistingProxyBillsUs(error.existing, {
|
|
92922
|
+
listenPort,
|
|
92923
|
+
authMode,
|
|
92924
|
+
apiKeyLabel: ourApiKeyLabel,
|
|
92925
|
+
paymentChain
|
|
92926
|
+
});
|
|
92801
92927
|
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
92802
92928
|
options.onReady?.(listenPort);
|
|
92803
92929
|
return {
|
|
92804
92930
|
port: listenPort,
|
|
92805
92931
|
baseUrl: baseUrl2,
|
|
92806
|
-
walletAddress: error.wallet,
|
|
92932
|
+
walletAddress: error.existing.wallet,
|
|
92807
92933
|
authMode,
|
|
92808
|
-
...
|
|
92934
|
+
...ourApiKeyLabel ? { apiKeyLabel: ourApiKeyLabel } : {},
|
|
92809
92935
|
balanceMonitor,
|
|
92810
92936
|
close: async () => {
|
|
92811
92937
|
}
|
|
@@ -93270,6 +93396,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
93270
93396
|
size: imageSize,
|
|
93271
93397
|
n: 1
|
|
93272
93398
|
});
|
|
93399
|
+
publishDispatchCost(estimateImageCost(imageModel, imageSize, 1));
|
|
93273
93400
|
const imageResponse = await payFetch(imageUpstreamUrl, {
|
|
93274
93401
|
method: "POST",
|
|
93275
93402
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -93551,6 +93678,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
93551
93678
|
size: img2imgSize,
|
|
93552
93679
|
n: 1
|
|
93553
93680
|
});
|
|
93681
|
+
publishDispatchCost(estimateImageCost(img2imgModel, img2imgSize, 1));
|
|
93554
93682
|
const img2imgResponse = await payFetch(`${apiBase}/v1/images/image2image`, {
|
|
93555
93683
|
method: "POST",
|
|
93556
93684
|
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
@@ -94380,6 +94508,8 @@ data: [DONE]
|
|
|
94380
94508
|
throw abortError();
|
|
94381
94509
|
}
|
|
94382
94510
|
console.log(`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`);
|
|
94511
|
+
const attemptEst = FREE_MODELS.has(tryModel) ? void 0 : estimateAmount(tryModel, body.length, maxTokens);
|
|
94512
|
+
publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
|
|
94383
94513
|
const perAttemptTimeoutMs = timeoutForModel(tryModel);
|
|
94384
94514
|
const modelController = new AbortController();
|
|
94385
94515
|
const modelTimeoutId = setTimeout(() => modelController.abort(), perAttemptTimeoutMs);
|
|
@@ -94479,6 +94609,7 @@ data: [DONE]
|
|
|
94479
94609
|
);
|
|
94480
94610
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
94481
94611
|
if (!globalController.signal.aborted) {
|
|
94612
|
+
publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
|
|
94482
94613
|
const retryController = new AbortController();
|
|
94483
94614
|
const retryTimeoutId = setTimeout(
|
|
94484
94615
|
() => retryController.abort(),
|
|
@@ -94541,6 +94672,7 @@ data: [DONE]
|
|
|
94541
94672
|
);
|
|
94542
94673
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
94543
94674
|
if (!globalController.signal.aborted) {
|
|
94675
|
+
publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
|
|
94544
94676
|
const retryController = new AbortController();
|
|
94545
94677
|
const retryTimeoutId = setTimeout(
|
|
94546
94678
|
() => retryController.abort(),
|
|
@@ -228050,6 +228182,19 @@ function isBlockrunWebSearchDisabled(config) {
|
|
|
228050
228182
|
const cfg = config ?? {};
|
|
228051
228183
|
return cfg.tools?.web?.search?.enabled === false;
|
|
228052
228184
|
}
|
|
228185
|
+
function legacyPackageIsBlockRun() {
|
|
228186
|
+
for (const dir of [
|
|
228187
|
+
join17(homedir14(), ".openclaw", "extensions", "clawrouter"),
|
|
228188
|
+
join17(homedir14(), ".openclaw", "npm", "node_modules", "@blockrun", "clawrouter")
|
|
228189
|
+
]) {
|
|
228190
|
+
try {
|
|
228191
|
+
const metadata = JSON.parse(readTextFileSync(join17(dir, "package.json")));
|
|
228192
|
+
if (metadata.name === "@blockrun/clawrouter") return true;
|
|
228193
|
+
} catch {
|
|
228194
|
+
}
|
|
228195
|
+
}
|
|
228196
|
+
return false;
|
|
228197
|
+
}
|
|
228053
228198
|
function injectModelsConfig(logger48, options = {}) {
|
|
228054
228199
|
const configDir = join17(homedir14(), ".openclaw");
|
|
228055
228200
|
const configPath = join17(configDir, "openclaw.json");
|
|
@@ -228213,9 +228358,9 @@ function injectModelsConfig(logger48, options = {}) {
|
|
|
228213
228358
|
const legacyEntry = config.plugins?.entries?.clawrouter;
|
|
228214
228359
|
const legacyEntryConfig = legacyEntry?.config;
|
|
228215
228360
|
const legacyBlockRunInstall = Boolean(
|
|
228216
|
-
legacyEntry && ["walletKey", "routing"].some(
|
|
228361
|
+
legacyEntry && (["walletKey", "routing"].some(
|
|
228217
228362
|
(key2) => key2 in legacyEntry || (legacyEntryConfig ? key2 in legacyEntryConfig : false)
|
|
228218
|
-
)
|
|
228363
|
+
) || legacyPackageIsBlockRun())
|
|
228219
228364
|
);
|
|
228220
228365
|
if (legacyBlockRunInstall && prepareBlockRunPluginConfig(config, { legacyBlockRunInstall })) {
|
|
228221
228366
|
needsWrite = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/clawrouter",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.275",
|
|
4
4
|
"description": "Smart LLM router — save 84% on inference costs. 76 models (7 free). Pay by credit card (API key) or USDC (x402 wallet).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"test:resilience:full": "npm run test:resilience:errors && npm run test:resilience:lifecycle && npm run test:resilience:stability:full",
|
|
59
59
|
"test:e2e": "tsx test/test-e2e.ts",
|
|
60
60
|
"test:e2e:tool-ids": "npm run build && tsx test/e2e-tool-id-sanitization.ts",
|
|
61
|
+
"test:e2e:openclaw-collision": "npm run build && tsx test/openclaw-plugin-collision-e2e.ts",
|
|
61
62
|
"test:docker:install": "bash test/run-docker-test.sh",
|
|
62
63
|
"test:docker:edge-cases": "bash test/docker/run-edge-cases.sh",
|
|
63
64
|
"test:docker:integration": "docker compose -f test/integration/docker-compose.yml up --build --abort-on-container-exit --exit-code-from integration"
|