@blockrun/clawrouter 0.3.1 → 0.3.3
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 +291 -217
- package/dist/index.d.ts +215 -3
- package/dist/index.js +668 -86
- package/dist/index.js.map +1 -1
- package/package.json +10 -3
package/dist/index.js
CHANGED
|
@@ -95,40 +95,260 @@ var envKeyAuth = {
|
|
|
95
95
|
// src/models.ts
|
|
96
96
|
var BLOCKRUN_MODELS = [
|
|
97
97
|
// Smart routing meta-model — proxy replaces with actual model
|
|
98
|
-
{
|
|
98
|
+
{
|
|
99
|
+
id: "blockrun/auto",
|
|
100
|
+
name: "BlockRun Smart Router",
|
|
101
|
+
inputPrice: 0,
|
|
102
|
+
outputPrice: 0,
|
|
103
|
+
contextWindow: 105e4,
|
|
104
|
+
maxOutput: 128e3
|
|
105
|
+
},
|
|
99
106
|
// OpenAI GPT-5 Family
|
|
100
|
-
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
{
|
|
108
|
+
id: "openai/gpt-5.2",
|
|
109
|
+
name: "GPT-5.2",
|
|
110
|
+
inputPrice: 1.75,
|
|
111
|
+
outputPrice: 14,
|
|
112
|
+
contextWindow: 4e5,
|
|
113
|
+
maxOutput: 128e3,
|
|
114
|
+
reasoning: true,
|
|
115
|
+
vision: true
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "openai/gpt-5-mini",
|
|
119
|
+
name: "GPT-5 Mini",
|
|
120
|
+
inputPrice: 0.25,
|
|
121
|
+
outputPrice: 2,
|
|
122
|
+
contextWindow: 2e5,
|
|
123
|
+
maxOutput: 65536
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "openai/gpt-5-nano",
|
|
127
|
+
name: "GPT-5 Nano",
|
|
128
|
+
inputPrice: 0.05,
|
|
129
|
+
outputPrice: 0.4,
|
|
130
|
+
contextWindow: 128e3,
|
|
131
|
+
maxOutput: 32768
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "openai/gpt-5.2-pro",
|
|
135
|
+
name: "GPT-5.2 Pro",
|
|
136
|
+
inputPrice: 21,
|
|
137
|
+
outputPrice: 168,
|
|
138
|
+
contextWindow: 4e5,
|
|
139
|
+
maxOutput: 128e3,
|
|
140
|
+
reasoning: true
|
|
141
|
+
},
|
|
104
142
|
// OpenAI GPT-4 Family
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
143
|
+
{
|
|
144
|
+
id: "openai/gpt-4.1",
|
|
145
|
+
name: "GPT-4.1",
|
|
146
|
+
inputPrice: 2,
|
|
147
|
+
outputPrice: 8,
|
|
148
|
+
contextWindow: 128e3,
|
|
149
|
+
maxOutput: 16384,
|
|
150
|
+
vision: true
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "openai/gpt-4.1-mini",
|
|
154
|
+
name: "GPT-4.1 Mini",
|
|
155
|
+
inputPrice: 0.4,
|
|
156
|
+
outputPrice: 1.6,
|
|
157
|
+
contextWindow: 128e3,
|
|
158
|
+
maxOutput: 16384
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "openai/gpt-4.1-nano",
|
|
162
|
+
name: "GPT-4.1 Nano",
|
|
163
|
+
inputPrice: 0.1,
|
|
164
|
+
outputPrice: 0.4,
|
|
165
|
+
contextWindow: 128e3,
|
|
166
|
+
maxOutput: 16384
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "openai/gpt-4o",
|
|
170
|
+
name: "GPT-4o",
|
|
171
|
+
inputPrice: 2.5,
|
|
172
|
+
outputPrice: 10,
|
|
173
|
+
contextWindow: 128e3,
|
|
174
|
+
maxOutput: 16384,
|
|
175
|
+
vision: true
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "openai/gpt-4o-mini",
|
|
179
|
+
name: "GPT-4o Mini",
|
|
180
|
+
inputPrice: 0.15,
|
|
181
|
+
outputPrice: 0.6,
|
|
182
|
+
contextWindow: 128e3,
|
|
183
|
+
maxOutput: 16384
|
|
184
|
+
},
|
|
110
185
|
// OpenAI O-series (Reasoning)
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
186
|
+
{
|
|
187
|
+
id: "openai/o1",
|
|
188
|
+
name: "o1",
|
|
189
|
+
inputPrice: 15,
|
|
190
|
+
outputPrice: 60,
|
|
191
|
+
contextWindow: 2e5,
|
|
192
|
+
maxOutput: 1e5,
|
|
193
|
+
reasoning: true
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: "openai/o1-mini",
|
|
197
|
+
name: "o1-mini",
|
|
198
|
+
inputPrice: 1.1,
|
|
199
|
+
outputPrice: 4.4,
|
|
200
|
+
contextWindow: 128e3,
|
|
201
|
+
maxOutput: 65536,
|
|
202
|
+
reasoning: true
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "openai/o3",
|
|
206
|
+
name: "o3",
|
|
207
|
+
inputPrice: 2,
|
|
208
|
+
outputPrice: 8,
|
|
209
|
+
contextWindow: 2e5,
|
|
210
|
+
maxOutput: 1e5,
|
|
211
|
+
reasoning: true
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "openai/o3-mini",
|
|
215
|
+
name: "o3-mini",
|
|
216
|
+
inputPrice: 1.1,
|
|
217
|
+
outputPrice: 4.4,
|
|
218
|
+
contextWindow: 128e3,
|
|
219
|
+
maxOutput: 65536,
|
|
220
|
+
reasoning: true
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "openai/o4-mini",
|
|
224
|
+
name: "o4-mini",
|
|
225
|
+
inputPrice: 1.1,
|
|
226
|
+
outputPrice: 4.4,
|
|
227
|
+
contextWindow: 128e3,
|
|
228
|
+
maxOutput: 65536,
|
|
229
|
+
reasoning: true
|
|
230
|
+
},
|
|
116
231
|
// Anthropic
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
232
|
+
{
|
|
233
|
+
id: "anthropic/claude-haiku-4.5",
|
|
234
|
+
name: "Claude Haiku 4.5",
|
|
235
|
+
inputPrice: 1,
|
|
236
|
+
outputPrice: 5,
|
|
237
|
+
contextWindow: 2e5,
|
|
238
|
+
maxOutput: 8192
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "anthropic/claude-sonnet-4",
|
|
242
|
+
name: "Claude Sonnet 4",
|
|
243
|
+
inputPrice: 3,
|
|
244
|
+
outputPrice: 15,
|
|
245
|
+
contextWindow: 2e5,
|
|
246
|
+
maxOutput: 64e3,
|
|
247
|
+
reasoning: true
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: "anthropic/claude-opus-4",
|
|
251
|
+
name: "Claude Opus 4",
|
|
252
|
+
inputPrice: 15,
|
|
253
|
+
outputPrice: 75,
|
|
254
|
+
contextWindow: 2e5,
|
|
255
|
+
maxOutput: 32e3,
|
|
256
|
+
reasoning: true
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "anthropic/claude-opus-4.5",
|
|
260
|
+
name: "Claude Opus 4.5",
|
|
261
|
+
inputPrice: 5,
|
|
262
|
+
outputPrice: 25,
|
|
263
|
+
contextWindow: 2e5,
|
|
264
|
+
maxOutput: 32e3,
|
|
265
|
+
reasoning: true
|
|
266
|
+
},
|
|
121
267
|
// Google
|
|
122
|
-
{
|
|
123
|
-
|
|
124
|
-
|
|
268
|
+
{
|
|
269
|
+
id: "google/gemini-3-pro-preview",
|
|
270
|
+
name: "Gemini 3 Pro Preview",
|
|
271
|
+
inputPrice: 2,
|
|
272
|
+
outputPrice: 12,
|
|
273
|
+
contextWindow: 105e4,
|
|
274
|
+
maxOutput: 65536,
|
|
275
|
+
reasoning: true,
|
|
276
|
+
vision: true
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: "google/gemini-2.5-pro",
|
|
280
|
+
name: "Gemini 2.5 Pro",
|
|
281
|
+
inputPrice: 1.25,
|
|
282
|
+
outputPrice: 10,
|
|
283
|
+
contextWindow: 105e4,
|
|
284
|
+
maxOutput: 65536,
|
|
285
|
+
reasoning: true,
|
|
286
|
+
vision: true
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "google/gemini-2.5-flash",
|
|
290
|
+
name: "Gemini 2.5 Flash",
|
|
291
|
+
inputPrice: 0.15,
|
|
292
|
+
outputPrice: 0.6,
|
|
293
|
+
contextWindow: 1e6,
|
|
294
|
+
maxOutput: 65536
|
|
295
|
+
},
|
|
125
296
|
// DeepSeek
|
|
126
|
-
{
|
|
127
|
-
|
|
297
|
+
{
|
|
298
|
+
id: "deepseek/deepseek-chat",
|
|
299
|
+
name: "DeepSeek V3.2 Chat",
|
|
300
|
+
inputPrice: 0.28,
|
|
301
|
+
outputPrice: 0.42,
|
|
302
|
+
contextWindow: 128e3,
|
|
303
|
+
maxOutput: 8192
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: "deepseek/deepseek-reasoner",
|
|
307
|
+
name: "DeepSeek V3.2 Reasoner",
|
|
308
|
+
inputPrice: 0.28,
|
|
309
|
+
outputPrice: 0.42,
|
|
310
|
+
contextWindow: 128e3,
|
|
311
|
+
maxOutput: 8192,
|
|
312
|
+
reasoning: true
|
|
313
|
+
},
|
|
314
|
+
// Moonshot / Kimi
|
|
315
|
+
{
|
|
316
|
+
id: "moonshot/kimi-k2.5",
|
|
317
|
+
name: "Kimi K2.5",
|
|
318
|
+
inputPrice: 0.5,
|
|
319
|
+
outputPrice: 2.4,
|
|
320
|
+
contextWindow: 262144,
|
|
321
|
+
maxOutput: 8192,
|
|
322
|
+
reasoning: true,
|
|
323
|
+
vision: true
|
|
324
|
+
},
|
|
128
325
|
// xAI / Grok
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
326
|
+
{
|
|
327
|
+
id: "xai/grok-3",
|
|
328
|
+
name: "Grok 3",
|
|
329
|
+
inputPrice: 3,
|
|
330
|
+
outputPrice: 15,
|
|
331
|
+
contextWindow: 131072,
|
|
332
|
+
maxOutput: 16384,
|
|
333
|
+
reasoning: true
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: "xai/grok-3-fast",
|
|
337
|
+
name: "Grok 3 Fast",
|
|
338
|
+
inputPrice: 5,
|
|
339
|
+
outputPrice: 25,
|
|
340
|
+
contextWindow: 131072,
|
|
341
|
+
maxOutput: 16384,
|
|
342
|
+
reasoning: true
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: "xai/grok-3-mini",
|
|
346
|
+
name: "Grok 3 Mini",
|
|
347
|
+
inputPrice: 0.3,
|
|
348
|
+
outputPrice: 0.5,
|
|
349
|
+
contextWindow: 131072,
|
|
350
|
+
maxOutput: 16384
|
|
351
|
+
}
|
|
132
352
|
];
|
|
133
353
|
function toOpenClawModel(m) {
|
|
134
354
|
return {
|
|
@@ -324,7 +544,16 @@ function createPaymentFetch(privateKey) {
|
|
|
324
544
|
if (paymentHeader2) {
|
|
325
545
|
return handle402(input, init, url, endpointPath, paymentHeader2);
|
|
326
546
|
}
|
|
327
|
-
|
|
547
|
+
paymentCache.invalidate(endpointPath);
|
|
548
|
+
const cleanResponse = await fetch(input, init);
|
|
549
|
+
if (cleanResponse.status !== 402) {
|
|
550
|
+
return cleanResponse;
|
|
551
|
+
}
|
|
552
|
+
const cleanHeader = cleanResponse.headers.get("x-payment-required");
|
|
553
|
+
if (!cleanHeader) {
|
|
554
|
+
throw new Error("402 response missing x-payment-required header");
|
|
555
|
+
}
|
|
556
|
+
return handle402(input, init, url, endpointPath, cleanHeader);
|
|
328
557
|
}
|
|
329
558
|
const response = await fetch(input, init);
|
|
330
559
|
if (response.status !== 402) {
|
|
@@ -384,10 +613,18 @@ function scoreTokenCount(estimatedTokens, thresholds) {
|
|
|
384
613
|
function scoreKeywordMatch(text, keywords, name, signalLabel, thresholds, scores) {
|
|
385
614
|
const matches = keywords.filter((kw) => text.includes(kw.toLowerCase()));
|
|
386
615
|
if (matches.length >= thresholds.high) {
|
|
387
|
-
return {
|
|
616
|
+
return {
|
|
617
|
+
name,
|
|
618
|
+
score: scores.high,
|
|
619
|
+
signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})`
|
|
620
|
+
};
|
|
388
621
|
}
|
|
389
622
|
if (matches.length >= thresholds.low) {
|
|
390
|
-
return {
|
|
623
|
+
return {
|
|
624
|
+
name,
|
|
625
|
+
score: scores.low,
|
|
626
|
+
signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})`
|
|
627
|
+
};
|
|
391
628
|
}
|
|
392
629
|
return { name, score: scores.none, signal: null };
|
|
393
630
|
}
|
|
@@ -510,9 +747,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
|
|
|
510
747
|
const w = weights[d.name] ?? 0;
|
|
511
748
|
weightedScore += d.score * w;
|
|
512
749
|
}
|
|
513
|
-
const reasoningMatches = config.reasoningKeywords.filter(
|
|
514
|
-
(kw) => text.includes(kw.toLowerCase())
|
|
515
|
-
);
|
|
750
|
+
const reasoningMatches = config.reasoningKeywords.filter((kw) => text.includes(kw.toLowerCase()));
|
|
516
751
|
if (reasoningMatches.length >= 2) {
|
|
517
752
|
const confidence2 = calibrateConfidence(
|
|
518
753
|
Math.max(weightedScore, 0.3),
|
|
@@ -534,10 +769,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
|
|
|
534
769
|
distanceFromBoundary = simpleMedium - weightedScore;
|
|
535
770
|
} else if (weightedScore < mediumComplex) {
|
|
536
771
|
tier = "MEDIUM";
|
|
537
|
-
distanceFromBoundary = Math.min(
|
|
538
|
-
weightedScore - simpleMedium,
|
|
539
|
-
mediumComplex - weightedScore
|
|
540
|
-
);
|
|
772
|
+
distanceFromBoundary = Math.min(weightedScore - simpleMedium, mediumComplex - weightedScore);
|
|
541
773
|
} else if (weightedScore < complexReasoning) {
|
|
542
774
|
tier = "COMPLEX";
|
|
543
775
|
distanceFromBoundary = Math.min(
|
|
@@ -642,15 +874,7 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
642
874
|
"database",
|
|
643
875
|
"infrastructure"
|
|
644
876
|
],
|
|
645
|
-
creativeKeywords: [
|
|
646
|
-
"story",
|
|
647
|
-
"poem",
|
|
648
|
-
"compose",
|
|
649
|
-
"brainstorm",
|
|
650
|
-
"creative",
|
|
651
|
-
"imagine",
|
|
652
|
-
"write a"
|
|
653
|
-
],
|
|
877
|
+
creativeKeywords: ["story", "poem", "compose", "brainstorm", "creative", "imagine", "write a"],
|
|
654
878
|
// New dimension keyword lists
|
|
655
879
|
imperativeVerbs: [
|
|
656
880
|
"build",
|
|
@@ -793,15 +1017,10 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
|
|
|
793
1017
|
);
|
|
794
1018
|
}
|
|
795
1019
|
const hasStructuredOutput = systemPrompt ? /json|structured|schema/i.test(systemPrompt) : false;
|
|
796
|
-
const ruleResult = classifyByRules(
|
|
797
|
-
prompt,
|
|
798
|
-
systemPrompt,
|
|
799
|
-
estimatedTokens,
|
|
800
|
-
config.scoring
|
|
801
|
-
);
|
|
1020
|
+
const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
|
|
802
1021
|
let tier;
|
|
803
1022
|
let confidence;
|
|
804
|
-
|
|
1023
|
+
const method = "rules";
|
|
805
1024
|
let reasoning = `score=${ruleResult.score} | ${ruleResult.signals.join(", ")}`;
|
|
806
1025
|
if (ruleResult.tier !== null) {
|
|
807
1026
|
tier = ruleResult.tier;
|
|
@@ -882,14 +1101,16 @@ var RequestDeduplicator = class {
|
|
|
882
1101
|
const entry = this.inflight.get(key);
|
|
883
1102
|
if (!entry) return void 0;
|
|
884
1103
|
const promise = new Promise((resolve) => {
|
|
885
|
-
entry.waiters.push(
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1104
|
+
entry.waiters.push(
|
|
1105
|
+
new Promise((r) => {
|
|
1106
|
+
const orig = entry.resolve;
|
|
1107
|
+
entry.resolve = (result) => {
|
|
1108
|
+
orig(result);
|
|
1109
|
+
resolve(result);
|
|
1110
|
+
r(result);
|
|
1111
|
+
};
|
|
1112
|
+
})
|
|
1113
|
+
);
|
|
893
1114
|
});
|
|
894
1115
|
return promise;
|
|
895
1116
|
}
|
|
@@ -928,11 +1149,182 @@ var RequestDeduplicator = class {
|
|
|
928
1149
|
}
|
|
929
1150
|
};
|
|
930
1151
|
|
|
1152
|
+
// src/balance.ts
|
|
1153
|
+
import { createPublicClient, http, erc20Abi } from "viem";
|
|
1154
|
+
import { base } from "viem/chains";
|
|
1155
|
+
|
|
1156
|
+
// src/errors.ts
|
|
1157
|
+
var InsufficientFundsError = class extends Error {
|
|
1158
|
+
code = "INSUFFICIENT_FUNDS";
|
|
1159
|
+
currentBalanceUSD;
|
|
1160
|
+
requiredUSD;
|
|
1161
|
+
walletAddress;
|
|
1162
|
+
constructor(opts) {
|
|
1163
|
+
super(
|
|
1164
|
+
`Insufficient USDC balance. Current: ${opts.currentBalanceUSD}, Required: ${opts.requiredUSD}. Fund wallet: ${opts.walletAddress}`
|
|
1165
|
+
);
|
|
1166
|
+
this.name = "InsufficientFundsError";
|
|
1167
|
+
this.currentBalanceUSD = opts.currentBalanceUSD;
|
|
1168
|
+
this.requiredUSD = opts.requiredUSD;
|
|
1169
|
+
this.walletAddress = opts.walletAddress;
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
var EmptyWalletError = class extends Error {
|
|
1173
|
+
code = "EMPTY_WALLET";
|
|
1174
|
+
walletAddress;
|
|
1175
|
+
constructor(walletAddress) {
|
|
1176
|
+
super(`No USDC balance. Fund wallet to use ClawRouter: ${walletAddress}`);
|
|
1177
|
+
this.name = "EmptyWalletError";
|
|
1178
|
+
this.walletAddress = walletAddress;
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
function isInsufficientFundsError(error) {
|
|
1182
|
+
return error instanceof Error && error.code === "INSUFFICIENT_FUNDS";
|
|
1183
|
+
}
|
|
1184
|
+
function isEmptyWalletError(error) {
|
|
1185
|
+
return error instanceof Error && error.code === "EMPTY_WALLET";
|
|
1186
|
+
}
|
|
1187
|
+
function isBalanceError(error) {
|
|
1188
|
+
return isInsufficientFundsError(error) || isEmptyWalletError(error);
|
|
1189
|
+
}
|
|
1190
|
+
var RpcError = class extends Error {
|
|
1191
|
+
code = "RPC_ERROR";
|
|
1192
|
+
originalError;
|
|
1193
|
+
constructor(message, originalError) {
|
|
1194
|
+
super(`RPC error: ${message}. Check network connectivity.`);
|
|
1195
|
+
this.name = "RpcError";
|
|
1196
|
+
this.originalError = originalError;
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
function isRpcError(error) {
|
|
1200
|
+
return error instanceof Error && error.code === "RPC_ERROR";
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/balance.ts
|
|
1204
|
+
var USDC_BASE2 = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
1205
|
+
var CACHE_TTL_MS = 3e4;
|
|
1206
|
+
var BALANCE_THRESHOLDS = {
|
|
1207
|
+
/** Low balance warning threshold: $1.00 */
|
|
1208
|
+
LOW_BALANCE_MICROS: 1000000n,
|
|
1209
|
+
/** Effectively zero threshold: $0.0001 (covers dust/rounding) */
|
|
1210
|
+
ZERO_THRESHOLD: 100n
|
|
1211
|
+
};
|
|
1212
|
+
var BalanceMonitor = class {
|
|
1213
|
+
client;
|
|
1214
|
+
walletAddress;
|
|
1215
|
+
/** Cached balance (null = not yet fetched) */
|
|
1216
|
+
cachedBalance = null;
|
|
1217
|
+
/** Timestamp when cache was last updated */
|
|
1218
|
+
cachedAt = 0;
|
|
1219
|
+
constructor(walletAddress) {
|
|
1220
|
+
this.walletAddress = walletAddress;
|
|
1221
|
+
this.client = createPublicClient({
|
|
1222
|
+
chain: base,
|
|
1223
|
+
transport: http()
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Check current USDC balance.
|
|
1228
|
+
* Uses cache if valid, otherwise fetches from RPC.
|
|
1229
|
+
*/
|
|
1230
|
+
async checkBalance() {
|
|
1231
|
+
const now = Date.now();
|
|
1232
|
+
if (this.cachedBalance !== null && now - this.cachedAt < CACHE_TTL_MS) {
|
|
1233
|
+
return this.buildInfo(this.cachedBalance);
|
|
1234
|
+
}
|
|
1235
|
+
const balance = await this.fetchBalance();
|
|
1236
|
+
this.cachedBalance = balance;
|
|
1237
|
+
this.cachedAt = now;
|
|
1238
|
+
return this.buildInfo(balance);
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Check if balance is sufficient for an estimated cost.
|
|
1242
|
+
*
|
|
1243
|
+
* @param estimatedCostMicros - Estimated cost in USDC smallest unit (6 decimals)
|
|
1244
|
+
*/
|
|
1245
|
+
async checkSufficient(estimatedCostMicros) {
|
|
1246
|
+
const info = await this.checkBalance();
|
|
1247
|
+
if (info.balance >= estimatedCostMicros) {
|
|
1248
|
+
return { sufficient: true, info };
|
|
1249
|
+
}
|
|
1250
|
+
const shortfall = estimatedCostMicros - info.balance;
|
|
1251
|
+
return {
|
|
1252
|
+
sufficient: false,
|
|
1253
|
+
info,
|
|
1254
|
+
shortfall: this.formatUSDC(shortfall)
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Optimistically deduct estimated cost from cached balance.
|
|
1259
|
+
* Call this after a successful payment to keep cache accurate.
|
|
1260
|
+
*
|
|
1261
|
+
* @param amountMicros - Amount to deduct in USDC smallest unit
|
|
1262
|
+
*/
|
|
1263
|
+
deductEstimated(amountMicros) {
|
|
1264
|
+
if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
|
|
1265
|
+
this.cachedBalance -= amountMicros;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Invalidate cache, forcing next checkBalance() to fetch from RPC.
|
|
1270
|
+
* Call this after a payment failure to get accurate balance.
|
|
1271
|
+
*/
|
|
1272
|
+
invalidate() {
|
|
1273
|
+
this.cachedBalance = null;
|
|
1274
|
+
this.cachedAt = 0;
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Force refresh balance from RPC (ignores cache).
|
|
1278
|
+
*/
|
|
1279
|
+
async refresh() {
|
|
1280
|
+
this.invalidate();
|
|
1281
|
+
return this.checkBalance();
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Format USDC amount (in micros) as "$X.XX".
|
|
1285
|
+
*/
|
|
1286
|
+
formatUSDC(amountMicros) {
|
|
1287
|
+
const dollars = Number(amountMicros) / 1e6;
|
|
1288
|
+
return `$${dollars.toFixed(2)}`;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Get the wallet address being monitored.
|
|
1292
|
+
*/
|
|
1293
|
+
getWalletAddress() {
|
|
1294
|
+
return this.walletAddress;
|
|
1295
|
+
}
|
|
1296
|
+
/** Fetch balance from RPC */
|
|
1297
|
+
async fetchBalance() {
|
|
1298
|
+
try {
|
|
1299
|
+
const balance = await this.client.readContract({
|
|
1300
|
+
address: USDC_BASE2,
|
|
1301
|
+
abi: erc20Abi,
|
|
1302
|
+
functionName: "balanceOf",
|
|
1303
|
+
args: [this.walletAddress]
|
|
1304
|
+
});
|
|
1305
|
+
return balance;
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
throw new RpcError(error instanceof Error ? error.message : "Unknown error", error);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
/** Build BalanceInfo from raw balance */
|
|
1311
|
+
buildInfo(balance) {
|
|
1312
|
+
return {
|
|
1313
|
+
balance,
|
|
1314
|
+
balanceUSD: this.formatUSDC(balance),
|
|
1315
|
+
isLow: balance < BALANCE_THRESHOLDS.LOW_BALANCE_MICROS,
|
|
1316
|
+
isEmpty: balance < BALANCE_THRESHOLDS.ZERO_THRESHOLD,
|
|
1317
|
+
walletAddress: this.walletAddress
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
|
|
931
1322
|
// src/proxy.ts
|
|
932
1323
|
var BLOCKRUN_API = "https://blockrun.ai/api";
|
|
933
1324
|
var AUTO_MODEL = "blockrun/auto";
|
|
934
|
-
var USER_AGENT = "clawrouter/0.3.
|
|
1325
|
+
var USER_AGENT = "clawrouter/0.3.2";
|
|
935
1326
|
var HEARTBEAT_INTERVAL_MS = 2e3;
|
|
1327
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
|
|
936
1328
|
function buildModelPricing() {
|
|
937
1329
|
const map = /* @__PURE__ */ new Map();
|
|
938
1330
|
for (const m of BLOCKRUN_MODELS) {
|
|
@@ -958,13 +1350,14 @@ function estimateAmount(modelId, bodyLength, maxTokens) {
|
|
|
958
1350
|
const estimatedInputTokens = Math.ceil(bodyLength / 4);
|
|
959
1351
|
const estimatedOutputTokens = maxTokens || model.maxOutput || 4096;
|
|
960
1352
|
const costUsd = estimatedInputTokens / 1e6 * model.inputPrice + estimatedOutputTokens / 1e6 * model.outputPrice;
|
|
961
|
-
const amountMicros = Math.ceil(costUsd * 1.2 * 1e6);
|
|
1353
|
+
const amountMicros = Math.max(100, Math.ceil(costUsd * 1.2 * 1e6));
|
|
962
1354
|
return amountMicros.toString();
|
|
963
1355
|
}
|
|
964
1356
|
async function startProxy(options) {
|
|
965
1357
|
const apiBase = options.apiBase ?? BLOCKRUN_API;
|
|
966
1358
|
const account = privateKeyToAccount3(options.walletKey);
|
|
967
|
-
const { fetch: payFetch
|
|
1359
|
+
const { fetch: payFetch } = createPaymentFetch(options.walletKey);
|
|
1360
|
+
const balanceMonitor = new BalanceMonitor(account.address);
|
|
968
1361
|
const routingConfig = mergeRoutingConfig(options.routingConfig);
|
|
969
1362
|
const modelPricing = buildModelPricing();
|
|
970
1363
|
const routerOpts = {
|
|
@@ -973,9 +1366,25 @@ async function startProxy(options) {
|
|
|
973
1366
|
};
|
|
974
1367
|
const deduplicator = new RequestDeduplicator();
|
|
975
1368
|
const server = createServer(async (req, res) => {
|
|
976
|
-
if (req.url === "/health") {
|
|
1369
|
+
if (req.url === "/health" || req.url?.startsWith("/health?")) {
|
|
1370
|
+
const url = new URL(req.url, "http://localhost");
|
|
1371
|
+
const full = url.searchParams.get("full") === "true";
|
|
1372
|
+
const response = {
|
|
1373
|
+
status: "ok",
|
|
1374
|
+
wallet: account.address
|
|
1375
|
+
};
|
|
1376
|
+
if (full) {
|
|
1377
|
+
try {
|
|
1378
|
+
const balanceInfo = await balanceMonitor.checkBalance();
|
|
1379
|
+
response.balance = balanceInfo.balanceUSD;
|
|
1380
|
+
response.isLow = balanceInfo.isLow;
|
|
1381
|
+
response.isEmpty = balanceInfo.isEmpty;
|
|
1382
|
+
} catch {
|
|
1383
|
+
response.balanceError = "Could not fetch balance";
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
977
1386
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
978
|
-
res.end(JSON.stringify(
|
|
1387
|
+
res.end(JSON.stringify(response));
|
|
979
1388
|
return;
|
|
980
1389
|
}
|
|
981
1390
|
if (!req.url?.startsWith("/v1")) {
|
|
@@ -984,19 +1393,32 @@ async function startProxy(options) {
|
|
|
984
1393
|
return;
|
|
985
1394
|
}
|
|
986
1395
|
try {
|
|
987
|
-
await proxyRequest(
|
|
1396
|
+
await proxyRequest(
|
|
1397
|
+
req,
|
|
1398
|
+
res,
|
|
1399
|
+
apiBase,
|
|
1400
|
+
payFetch,
|
|
1401
|
+
options,
|
|
1402
|
+
routerOpts,
|
|
1403
|
+
deduplicator,
|
|
1404
|
+
balanceMonitor
|
|
1405
|
+
);
|
|
988
1406
|
} catch (err) {
|
|
989
1407
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
990
1408
|
options.onError?.(error);
|
|
991
1409
|
if (!res.headersSent) {
|
|
992
1410
|
res.writeHead(502, { "Content-Type": "application/json" });
|
|
993
|
-
res.end(
|
|
994
|
-
|
|
995
|
-
|
|
1411
|
+
res.end(
|
|
1412
|
+
JSON.stringify({
|
|
1413
|
+
error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
|
|
1414
|
+
})
|
|
1415
|
+
);
|
|
996
1416
|
} else if (!res.writableEnded) {
|
|
997
|
-
res.write(
|
|
1417
|
+
res.write(
|
|
1418
|
+
`data: ${JSON.stringify({ error: { message: error.message, type: "proxy_error" } })}
|
|
998
1419
|
|
|
999
|
-
`
|
|
1420
|
+
`
|
|
1421
|
+
);
|
|
1000
1422
|
res.write("data: [DONE]\n\n");
|
|
1001
1423
|
res.end();
|
|
1002
1424
|
}
|
|
@@ -1013,6 +1435,8 @@ async function startProxy(options) {
|
|
|
1013
1435
|
resolve({
|
|
1014
1436
|
port,
|
|
1015
1437
|
baseUrl,
|
|
1438
|
+
walletAddress: account.address,
|
|
1439
|
+
balanceMonitor,
|
|
1016
1440
|
close: () => new Promise((res, rej) => {
|
|
1017
1441
|
server.close((err) => err ? rej(err) : res());
|
|
1018
1442
|
})
|
|
@@ -1020,7 +1444,7 @@ async function startProxy(options) {
|
|
|
1020
1444
|
});
|
|
1021
1445
|
});
|
|
1022
1446
|
}
|
|
1023
|
-
async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator) {
|
|
1447
|
+
async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor) {
|
|
1024
1448
|
const startTime = Date.now();
|
|
1025
1449
|
const upstreamUrl = `${apiBase}${req.url}`;
|
|
1026
1450
|
const bodyChunks = [];
|
|
@@ -1077,13 +1501,51 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
1077
1501
|
return;
|
|
1078
1502
|
}
|
|
1079
1503
|
deduplicator.markInflight(dedupKey);
|
|
1504
|
+
let estimatedCostMicros;
|
|
1505
|
+
if (modelId) {
|
|
1506
|
+
const estimated = estimateAmount(modelId, body.length, maxTokens);
|
|
1507
|
+
if (estimated) {
|
|
1508
|
+
estimatedCostMicros = BigInt(estimated);
|
|
1509
|
+
const sufficiency = await balanceMonitor.checkSufficient(estimatedCostMicros);
|
|
1510
|
+
if (sufficiency.info.isEmpty) {
|
|
1511
|
+
deduplicator.removeInflight(dedupKey);
|
|
1512
|
+
const error = new EmptyWalletError(sufficiency.info.walletAddress);
|
|
1513
|
+
options.onInsufficientFunds?.({
|
|
1514
|
+
balanceUSD: sufficiency.info.balanceUSD,
|
|
1515
|
+
requiredUSD: balanceMonitor.formatUSDC(estimatedCostMicros),
|
|
1516
|
+
walletAddress: sufficiency.info.walletAddress
|
|
1517
|
+
});
|
|
1518
|
+
throw error;
|
|
1519
|
+
}
|
|
1520
|
+
if (!sufficiency.sufficient) {
|
|
1521
|
+
deduplicator.removeInflight(dedupKey);
|
|
1522
|
+
const error = new InsufficientFundsError({
|
|
1523
|
+
currentBalanceUSD: sufficiency.info.balanceUSD,
|
|
1524
|
+
requiredUSD: balanceMonitor.formatUSDC(estimatedCostMicros),
|
|
1525
|
+
walletAddress: sufficiency.info.walletAddress
|
|
1526
|
+
});
|
|
1527
|
+
options.onInsufficientFunds?.({
|
|
1528
|
+
balanceUSD: sufficiency.info.balanceUSD,
|
|
1529
|
+
requiredUSD: balanceMonitor.formatUSDC(estimatedCostMicros),
|
|
1530
|
+
walletAddress: sufficiency.info.walletAddress
|
|
1531
|
+
});
|
|
1532
|
+
throw error;
|
|
1533
|
+
}
|
|
1534
|
+
if (sufficiency.info.isLow) {
|
|
1535
|
+
options.onLowBalance?.({
|
|
1536
|
+
balanceUSD: sufficiency.info.balanceUSD,
|
|
1537
|
+
walletAddress: sufficiency.info.walletAddress
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1080
1542
|
let heartbeatInterval;
|
|
1081
1543
|
let headersSentEarly = false;
|
|
1082
1544
|
if (isStreaming) {
|
|
1083
1545
|
res.writeHead(200, {
|
|
1084
1546
|
"content-type": "text/event-stream",
|
|
1085
1547
|
"cache-control": "no-cache",
|
|
1086
|
-
|
|
1548
|
+
connection: "keep-alive"
|
|
1087
1549
|
});
|
|
1088
1550
|
headersSentEarly = true;
|
|
1089
1551
|
res.write(": heartbeat\n\n");
|
|
@@ -1095,7 +1557,8 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
1095
1557
|
}
|
|
1096
1558
|
const headers = {};
|
|
1097
1559
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
1098
|
-
if (key === "host" || key === "connection" || key === "transfer-encoding" || key === "content-length")
|
|
1560
|
+
if (key === "host" || key === "connection" || key === "transfer-encoding" || key === "content-length")
|
|
1561
|
+
continue;
|
|
1099
1562
|
if (typeof value === "string") {
|
|
1100
1563
|
headers[key] = value;
|
|
1101
1564
|
}
|
|
@@ -1105,18 +1568,34 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
1105
1568
|
}
|
|
1106
1569
|
headers["user-agent"] = USER_AGENT;
|
|
1107
1570
|
let preAuth;
|
|
1108
|
-
if (
|
|
1109
|
-
|
|
1110
|
-
if (estimated) {
|
|
1111
|
-
preAuth = { estimatedAmount: estimated };
|
|
1112
|
-
}
|
|
1571
|
+
if (estimatedCostMicros !== void 0) {
|
|
1572
|
+
preAuth = { estimatedAmount: estimatedCostMicros.toString() };
|
|
1113
1573
|
}
|
|
1574
|
+
let completed = false;
|
|
1575
|
+
res.on("close", () => {
|
|
1576
|
+
if (heartbeatInterval) {
|
|
1577
|
+
clearInterval(heartbeatInterval);
|
|
1578
|
+
heartbeatInterval = void 0;
|
|
1579
|
+
}
|
|
1580
|
+
if (!completed) {
|
|
1581
|
+
deduplicator.removeInflight(dedupKey);
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
1585
|
+
const controller = new AbortController();
|
|
1586
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1114
1587
|
try {
|
|
1115
|
-
const upstream = await payFetch(
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1588
|
+
const upstream = await payFetch(
|
|
1589
|
+
upstreamUrl,
|
|
1590
|
+
{
|
|
1591
|
+
method: req.method ?? "POST",
|
|
1592
|
+
headers,
|
|
1593
|
+
body: body.length > 0 ? body : void 0,
|
|
1594
|
+
signal: controller.signal
|
|
1595
|
+
},
|
|
1596
|
+
preAuth
|
|
1597
|
+
);
|
|
1598
|
+
clearTimeout(timeoutId);
|
|
1120
1599
|
if (heartbeatInterval) {
|
|
1121
1600
|
clearInterval(heartbeatInterval);
|
|
1122
1601
|
heartbeatInterval = void 0;
|
|
@@ -1189,11 +1668,21 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
1189
1668
|
completedAt: Date.now()
|
|
1190
1669
|
});
|
|
1191
1670
|
}
|
|
1671
|
+
if (estimatedCostMicros !== void 0) {
|
|
1672
|
+
balanceMonitor.deductEstimated(estimatedCostMicros);
|
|
1673
|
+
}
|
|
1674
|
+
completed = true;
|
|
1192
1675
|
} catch (err) {
|
|
1676
|
+
clearTimeout(timeoutId);
|
|
1193
1677
|
if (heartbeatInterval) {
|
|
1194
1678
|
clearInterval(heartbeatInterval);
|
|
1679
|
+
heartbeatInterval = void 0;
|
|
1195
1680
|
}
|
|
1196
1681
|
deduplicator.removeInflight(dedupKey);
|
|
1682
|
+
balanceMonitor.invalidate();
|
|
1683
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1684
|
+
throw new Error(`Request timed out after ${timeoutMs}ms`);
|
|
1685
|
+
}
|
|
1197
1686
|
throw err;
|
|
1198
1687
|
}
|
|
1199
1688
|
if (routingDecision) {
|
|
@@ -1208,6 +1697,62 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
1208
1697
|
}
|
|
1209
1698
|
}
|
|
1210
1699
|
|
|
1700
|
+
// src/retry.ts
|
|
1701
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
1702
|
+
maxRetries: 2,
|
|
1703
|
+
baseDelayMs: 500,
|
|
1704
|
+
retryableCodes: [429, 502, 503, 504]
|
|
1705
|
+
};
|
|
1706
|
+
function sleep(ms) {
|
|
1707
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1708
|
+
}
|
|
1709
|
+
async function fetchWithRetry(fetchFn, url, init, config) {
|
|
1710
|
+
const cfg = {
|
|
1711
|
+
...DEFAULT_RETRY_CONFIG,
|
|
1712
|
+
...config
|
|
1713
|
+
};
|
|
1714
|
+
let lastError;
|
|
1715
|
+
let lastResponse;
|
|
1716
|
+
for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
|
|
1717
|
+
try {
|
|
1718
|
+
const response = await fetchFn(url, init);
|
|
1719
|
+
if (!cfg.retryableCodes.includes(response.status)) {
|
|
1720
|
+
return response;
|
|
1721
|
+
}
|
|
1722
|
+
lastResponse = response;
|
|
1723
|
+
const retryAfter = response.headers.get("retry-after");
|
|
1724
|
+
let delay;
|
|
1725
|
+
if (retryAfter) {
|
|
1726
|
+
const seconds = parseInt(retryAfter, 10);
|
|
1727
|
+
delay = isNaN(seconds) ? cfg.baseDelayMs * Math.pow(2, attempt) : seconds * 1e3;
|
|
1728
|
+
} else {
|
|
1729
|
+
delay = cfg.baseDelayMs * Math.pow(2, attempt);
|
|
1730
|
+
}
|
|
1731
|
+
if (attempt < cfg.maxRetries) {
|
|
1732
|
+
await sleep(delay);
|
|
1733
|
+
}
|
|
1734
|
+
} catch (err) {
|
|
1735
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1736
|
+
if (attempt < cfg.maxRetries) {
|
|
1737
|
+
const delay = cfg.baseDelayMs * Math.pow(2, attempt);
|
|
1738
|
+
await sleep(delay);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
if (lastResponse) {
|
|
1743
|
+
return lastResponse;
|
|
1744
|
+
}
|
|
1745
|
+
throw lastError ?? new Error("Max retries exceeded");
|
|
1746
|
+
}
|
|
1747
|
+
function isRetryable(errorOrResponse, config) {
|
|
1748
|
+
const retryableCodes = config?.retryableCodes ?? DEFAULT_RETRY_CONFIG.retryableCodes;
|
|
1749
|
+
if (errorOrResponse instanceof Response) {
|
|
1750
|
+
return retryableCodes.includes(errorOrResponse.status);
|
|
1751
|
+
}
|
|
1752
|
+
const message = errorOrResponse.message.toLowerCase();
|
|
1753
|
+
return message.includes("network") || message.includes("timeout") || message.includes("econnreset") || message.includes("econnrefused") || message.includes("socket hang up");
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1211
1756
|
// src/index.ts
|
|
1212
1757
|
async function startProxyInBackground(api) {
|
|
1213
1758
|
const { key: walletKey, address, source } = await resolveOrGenerateWalletKey();
|
|
@@ -1219,6 +1764,23 @@ async function startProxyInBackground(api) {
|
|
|
1219
1764
|
} else {
|
|
1220
1765
|
api.logger.info(`Using wallet from BLOCKRUN_WALLET_KEY: ${address}`);
|
|
1221
1766
|
}
|
|
1767
|
+
const startupMonitor = new BalanceMonitor(address);
|
|
1768
|
+
try {
|
|
1769
|
+
const startupBalance = await startupMonitor.checkBalance();
|
|
1770
|
+
if (startupBalance.isEmpty) {
|
|
1771
|
+
api.logger.warn(`[!] No USDC balance. Fund wallet to use ClawRouter: ${address}`);
|
|
1772
|
+
} else if (startupBalance.isLow) {
|
|
1773
|
+
api.logger.warn(
|
|
1774
|
+
`[!] Low balance: ${startupBalance.balanceUSD} remaining. Fund wallet: ${address}`
|
|
1775
|
+
);
|
|
1776
|
+
} else {
|
|
1777
|
+
api.logger.info(`Wallet balance: ${startupBalance.balanceUSD}`);
|
|
1778
|
+
}
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
api.logger.warn(
|
|
1781
|
+
`Could not check wallet balance: ${err instanceof Error ? err.message : String(err)}`
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1222
1784
|
const routingConfig = api.pluginConfig?.routing;
|
|
1223
1785
|
const proxy = await startProxy({
|
|
1224
1786
|
walletKey,
|
|
@@ -1233,6 +1795,14 @@ async function startProxyInBackground(api) {
|
|
|
1233
1795
|
const cost = decision.costEstimate.toFixed(4);
|
|
1234
1796
|
const saved = (decision.savings * 100).toFixed(0);
|
|
1235
1797
|
api.logger.info(`${decision.model} $${cost} (saved ${saved}%)`);
|
|
1798
|
+
},
|
|
1799
|
+
onLowBalance: (info) => {
|
|
1800
|
+
api.logger.warn(`[!] Low balance: ${info.balanceUSD}. Fund wallet: ${info.walletAddress}`);
|
|
1801
|
+
},
|
|
1802
|
+
onInsufficientFunds: (info) => {
|
|
1803
|
+
api.logger.error(
|
|
1804
|
+
`[!] Insufficient funds. Balance: ${info.balanceUSD}, Needed: ${info.requiredUSD}. Fund wallet: ${info.walletAddress}`
|
|
1805
|
+
);
|
|
1236
1806
|
}
|
|
1237
1807
|
});
|
|
1238
1808
|
setActiveProxy(proxy);
|
|
@@ -1242,7 +1812,7 @@ var plugin = {
|
|
|
1242
1812
|
id: "clawrouter",
|
|
1243
1813
|
name: "ClawRouter",
|
|
1244
1814
|
description: "Smart LLM router \u2014 30+ models, x402 micropayments, 78% cost savings",
|
|
1245
|
-
version: "0.3.
|
|
1815
|
+
version: "0.3.2",
|
|
1246
1816
|
register(api) {
|
|
1247
1817
|
api.registerProvider(blockrunProvider);
|
|
1248
1818
|
api.logger.info("BlockRun provider registered (30+ models via x402)");
|
|
@@ -1255,15 +1825,27 @@ var plugin = {
|
|
|
1255
1825
|
};
|
|
1256
1826
|
var index_default = plugin;
|
|
1257
1827
|
export {
|
|
1828
|
+
BALANCE_THRESHOLDS,
|
|
1258
1829
|
BLOCKRUN_MODELS,
|
|
1830
|
+
BalanceMonitor,
|
|
1831
|
+
DEFAULT_RETRY_CONFIG,
|
|
1259
1832
|
DEFAULT_ROUTING_CONFIG,
|
|
1833
|
+
EmptyWalletError,
|
|
1834
|
+
InsufficientFundsError,
|
|
1260
1835
|
OPENCLAW_MODELS,
|
|
1261
1836
|
PaymentCache,
|
|
1262
1837
|
RequestDeduplicator,
|
|
1838
|
+
RpcError,
|
|
1263
1839
|
blockrunProvider,
|
|
1264
1840
|
buildProviderModels,
|
|
1265
1841
|
createPaymentFetch,
|
|
1266
1842
|
index_default as default,
|
|
1843
|
+
fetchWithRetry,
|
|
1844
|
+
isBalanceError,
|
|
1845
|
+
isEmptyWalletError,
|
|
1846
|
+
isInsufficientFundsError,
|
|
1847
|
+
isRetryable,
|
|
1848
|
+
isRpcError,
|
|
1267
1849
|
logUsage,
|
|
1268
1850
|
route,
|
|
1269
1851
|
startProxy
|