@blockrun/clawrouter 0.12.282 → 0.12.284
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 +23 -22
- package/dist/cli.js +281 -35
- package/dist/index.d.ts +14 -0
- package/dist/index.js +239 -28
- package/docs/9-free-ai-models-zero-cost-blockrun.md +5 -5
- package/docs/anthropic-cost-savings.md +1 -1
- package/docs/anthropic-third-party-harness-changes.md +3 -3
- package/docs/clawrouter-vs-openrouter-llm-routing-comparison.md +3 -3
- package/docs/configuration.md +6 -0
- package/package.json +2 -2
- package/skills/clawrouter/SKILL.md +3 -3
package/dist/index.js
CHANGED
|
@@ -34678,6 +34678,11 @@ function getSharedSpendControl() {
|
|
|
34678
34678
|
host.__clawrouterSharedSpendControl ??= new SpendControl();
|
|
34679
34679
|
return host.__clawrouterSharedSpendControl;
|
|
34680
34680
|
}
|
|
34681
|
+
function parseQuotedAmountMicros(selected) {
|
|
34682
|
+
const raw = selected.amount ?? selected.maxAmountRequired;
|
|
34683
|
+
if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) return void 0;
|
|
34684
|
+
return BigInt(raw);
|
|
34685
|
+
}
|
|
34681
34686
|
function parseQuotedAmountUsd(selected) {
|
|
34682
34687
|
const raw = selected.amount ?? selected.maxAmountRequired;
|
|
34683
34688
|
if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) {
|
|
@@ -35443,6 +35448,16 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
|
|
|
35443
35448
|
return est === void 0 ? void 0 : Number(est);
|
|
35444
35449
|
};
|
|
35445
35450
|
const needMicros = estimateMicros();
|
|
35451
|
+
const notifyDeclinedPayment = () => {
|
|
35452
|
+
if (!options?.onPaymentDeclined) return;
|
|
35453
|
+
try {
|
|
35454
|
+
options.onPaymentDeclined();
|
|
35455
|
+
} catch (error) {
|
|
35456
|
+
console.error(
|
|
35457
|
+
`[ClawRouter] onPaymentDeclined callback failed: ${error instanceof Error ? error.message : String(error)}`
|
|
35458
|
+
);
|
|
35459
|
+
}
|
|
35460
|
+
};
|
|
35446
35461
|
const cached = !options?.skipPreAuth ? cache2.get(cacheKey2) : void 0;
|
|
35447
35462
|
const preAuthCovers = cached !== void 0 && Date.now() - cached.cachedAt < ttlMs && cached.coverMicros !== void 0 && needMicros !== void 0 && needMicros <= cached.coverMicros;
|
|
35448
35463
|
if (preAuthCovers) {
|
|
@@ -35461,6 +35476,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
|
|
|
35461
35476
|
return response2;
|
|
35462
35477
|
}
|
|
35463
35478
|
paymentInFlight = false;
|
|
35479
|
+
notifyDeclinedPayment();
|
|
35464
35480
|
cache2.delete(cacheKey2);
|
|
35465
35481
|
} catch (err) {
|
|
35466
35482
|
cache2.delete(cacheKey2);
|
|
@@ -35505,6 +35521,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
|
|
|
35505
35521
|
clonedRequest.headers.set(key3, value);
|
|
35506
35522
|
}
|
|
35507
35523
|
const paidResponse = await baseFetch(clonedRequest);
|
|
35524
|
+
if (paidResponse.status === 402) notifyDeclinedPayment();
|
|
35508
35525
|
notifyAcceptedPayment(paidResponse, payload);
|
|
35509
35526
|
return paidResponse;
|
|
35510
35527
|
};
|
|
@@ -64542,6 +64559,132 @@ var init_api_key = __esm({
|
|
|
64542
64559
|
}
|
|
64543
64560
|
});
|
|
64544
64561
|
|
|
64562
|
+
// src/session-spend.ts
|
|
64563
|
+
function parseMicros(raw) {
|
|
64564
|
+
if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
|
|
64565
|
+
return BigInt(raw);
|
|
64566
|
+
}
|
|
64567
|
+
function usdToMicros(usd) {
|
|
64568
|
+
return BigInt(Math.round(usd * 1e6));
|
|
64569
|
+
}
|
|
64570
|
+
function microsToUsd(micros) {
|
|
64571
|
+
return Number(micros) / 1e6;
|
|
64572
|
+
}
|
|
64573
|
+
var SessionSpend, CostCapError;
|
|
64574
|
+
var init_session_spend = __esm({
|
|
64575
|
+
"src/session-spend.ts"() {
|
|
64576
|
+
"use strict";
|
|
64577
|
+
init_spend_control();
|
|
64578
|
+
SessionSpend = class _SessionSpend {
|
|
64579
|
+
constructor(store, sessionId, capMicros, holdMicros) {
|
|
64580
|
+
this.store = store;
|
|
64581
|
+
this.sessionId = sessionId;
|
|
64582
|
+
this.capMicros = capMicros;
|
|
64583
|
+
this.heldMicros = holdMicros;
|
|
64584
|
+
if (holdMicros > 0n) store.adjustSessionHold(sessionId, holdMicros);
|
|
64585
|
+
}
|
|
64586
|
+
store;
|
|
64587
|
+
sessionId;
|
|
64588
|
+
capMicros;
|
|
64589
|
+
/** Budget this request still holds against the session (strict mode only). */
|
|
64590
|
+
heldMicros;
|
|
64591
|
+
/** Sum of payments signed by this request that still count. */
|
|
64592
|
+
signedMicros = 0n;
|
|
64593
|
+
/** The most recent signature not yet settled or voided — the one a decline undoes. */
|
|
64594
|
+
lastSignedMicros = 0n;
|
|
64595
|
+
released = false;
|
|
64596
|
+
/**
|
|
64597
|
+
* Strict mode: admit the request only if the session's spend, plus what its
|
|
64598
|
+
* other in-flight requests hold, plus this request's estimate fits the cap —
|
|
64599
|
+
* and hold the estimate in the same step.
|
|
64600
|
+
*/
|
|
64601
|
+
static admit(store, sessionId, capMicros, estimateMicros) {
|
|
64602
|
+
const spentMicros = store.getSessionCostMicros(sessionId);
|
|
64603
|
+
const heldMicros = store.getSessionHoldMicros(sessionId);
|
|
64604
|
+
if (spentMicros + heldMicros + estimateMicros > capMicros) {
|
|
64605
|
+
return { admitted: false, spentMicros, heldMicros, estimateMicros };
|
|
64606
|
+
}
|
|
64607
|
+
return {
|
|
64608
|
+
admitted: true,
|
|
64609
|
+
spend: new _SessionSpend(store, sessionId, capMicros, estimateMicros)
|
|
64610
|
+
};
|
|
64611
|
+
}
|
|
64612
|
+
/** Graceful mode: track signed spend, enforce nothing. */
|
|
64613
|
+
static track(store, sessionId) {
|
|
64614
|
+
return new _SessionSpend(store, sessionId, void 0, 0n);
|
|
64615
|
+
}
|
|
64616
|
+
get isStrict() {
|
|
64617
|
+
return this.capMicros !== void 0;
|
|
64618
|
+
}
|
|
64619
|
+
/**
|
|
64620
|
+
* Before a payment is created: may this request sign `quoteMicros`?
|
|
64621
|
+
* Spend already recorded (including this request's own earlier signatures)
|
|
64622
|
+
* plus what OTHER in-flight requests hold, plus the quote, must fit the cap.
|
|
64623
|
+
* Always true in graceful mode.
|
|
64624
|
+
*/
|
|
64625
|
+
coverPayment(quoteMicros) {
|
|
64626
|
+
if (this.capMicros === void 0) return true;
|
|
64627
|
+
const spent = this.store.getSessionCostMicros(this.sessionId);
|
|
64628
|
+
const heldByOthers = this.store.getSessionHoldMicros(this.sessionId) - this.heldMicros;
|
|
64629
|
+
if (spent + heldByOthers + quoteMicros > this.capMicros) return false;
|
|
64630
|
+
if (quoteMicros > this.heldMicros) {
|
|
64631
|
+
this.store.adjustSessionHold(this.sessionId, quoteMicros - this.heldMicros);
|
|
64632
|
+
this.heldMicros = quoteMicros;
|
|
64633
|
+
}
|
|
64634
|
+
return true;
|
|
64635
|
+
}
|
|
64636
|
+
/** A payment was signed: it is spend now, and replaces part of the hold. */
|
|
64637
|
+
recordSigned(amountMicros) {
|
|
64638
|
+
if (amountMicros <= 0n) return;
|
|
64639
|
+
this.signedMicros += amountMicros;
|
|
64640
|
+
this.lastSignedMicros = amountMicros;
|
|
64641
|
+
this.store.addSessionCost(this.sessionId, amountMicros);
|
|
64642
|
+
this.shrinkHold(amountMicros);
|
|
64643
|
+
}
|
|
64644
|
+
/**
|
|
64645
|
+
* The gateway answered the payment just signed with a 402: nothing settled.
|
|
64646
|
+
* Payments within one request are signed and sent one at a time, so the
|
|
64647
|
+
* decline always belongs to the latest signature. Undone at most once.
|
|
64648
|
+
*/
|
|
64649
|
+
voidLastSigned() {
|
|
64650
|
+
const undo = this.lastSignedMicros;
|
|
64651
|
+
this.lastSignedMicros = 0n;
|
|
64652
|
+
if (undo <= 0n) return;
|
|
64653
|
+
this.signedMicros -= undo;
|
|
64654
|
+
this.store.addSessionCost(this.sessionId, -undo);
|
|
64655
|
+
}
|
|
64656
|
+
/**
|
|
64657
|
+
* An attempt succeeded. When nothing was signed for this request (the API
|
|
64658
|
+
* key rail), the local estimate is the only record of the spend. When a
|
|
64659
|
+
* payment was signed, it is already recorded at its real amount.
|
|
64660
|
+
*/
|
|
64661
|
+
recordSuccess(estimateMicros) {
|
|
64662
|
+
if (this.signedMicros > 0n || estimateMicros <= 0n) return;
|
|
64663
|
+
this.store.addSessionCost(this.sessionId, estimateMicros);
|
|
64664
|
+
this.shrinkHold(estimateMicros);
|
|
64665
|
+
}
|
|
64666
|
+
/** The request is over: return whatever it still holds. Idempotent. */
|
|
64667
|
+
release() {
|
|
64668
|
+
if (this.released) return;
|
|
64669
|
+
this.released = true;
|
|
64670
|
+
this.shrinkHold(this.heldMicros);
|
|
64671
|
+
}
|
|
64672
|
+
shrinkHold(byMicros) {
|
|
64673
|
+
const drop = byMicros < this.heldMicros ? byMicros : this.heldMicros;
|
|
64674
|
+
if (drop <= 0n) return;
|
|
64675
|
+
this.heldMicros -= drop;
|
|
64676
|
+
this.store.adjustSessionHold(this.sessionId, -drop);
|
|
64677
|
+
}
|
|
64678
|
+
};
|
|
64679
|
+
CostCapError = class extends SpendPolicyError {
|
|
64680
|
+
constructor(reason) {
|
|
64681
|
+
super(reason);
|
|
64682
|
+
this.name = "CostCapError";
|
|
64683
|
+
}
|
|
64684
|
+
};
|
|
64685
|
+
}
|
|
64686
|
+
});
|
|
64687
|
+
|
|
64545
64688
|
// src/compression/types.ts
|
|
64546
64689
|
var DEFAULT_COMPRESSION_CONFIG;
|
|
64547
64690
|
var init_types2 = __esm({
|
|
@@ -65389,6 +65532,9 @@ var init_session = __esm({
|
|
|
65389
65532
|
};
|
|
65390
65533
|
SessionStore = class {
|
|
65391
65534
|
sessions = /* @__PURE__ */ new Map();
|
|
65535
|
+
// Kept apart from `sessions`: a hold belongs to requests still running, so it
|
|
65536
|
+
// must not vanish when the idle-session sweep drops an entry mid-request.
|
|
65537
|
+
sessionHolds = /* @__PURE__ */ new Map();
|
|
65392
65538
|
config;
|
|
65393
65539
|
cleanupInterval = null;
|
|
65394
65540
|
constructor(config = {}) {
|
|
@@ -65564,14 +65710,34 @@ var init_session = __esm({
|
|
|
65564
65710
|
this.sessions.set(sessionId, entry);
|
|
65565
65711
|
}
|
|
65566
65712
|
entry.sessionCostMicros += additionalMicros;
|
|
65713
|
+
if (entry.sessionCostMicros < 0n) entry.sessionCostMicros = 0n;
|
|
65567
65714
|
}
|
|
65568
65715
|
/**
|
|
65569
65716
|
* Get the total accumulated cost for a session in USD.
|
|
65570
65717
|
*/
|
|
65571
65718
|
getSessionCostUsd(sessionId) {
|
|
65572
|
-
|
|
65573
|
-
|
|
65574
|
-
|
|
65719
|
+
return Number(this.getSessionCostMicros(sessionId)) / 1e6;
|
|
65720
|
+
}
|
|
65721
|
+
/**
|
|
65722
|
+
* Get the total accumulated cost for a session in USDC micros.
|
|
65723
|
+
*/
|
|
65724
|
+
getSessionCostMicros(sessionId) {
|
|
65725
|
+
return this.sessions.get(sessionId)?.sessionCostMicros ?? 0n;
|
|
65726
|
+
}
|
|
65727
|
+
/**
|
|
65728
|
+
* Budget held by a session's in-flight requests (USDC micros) — admitted
|
|
65729
|
+
* against the strict cap but not yet spent. See session-spend.ts.
|
|
65730
|
+
*/
|
|
65731
|
+
getSessionHoldMicros(sessionId) {
|
|
65732
|
+
return this.sessionHolds.get(sessionId) ?? 0n;
|
|
65733
|
+
}
|
|
65734
|
+
/**
|
|
65735
|
+
* Grow (positive) or release (negative) a session's in-flight hold.
|
|
65736
|
+
*/
|
|
65737
|
+
adjustSessionHold(sessionId, deltaMicros) {
|
|
65738
|
+
const next = (this.sessionHolds.get(sessionId) ?? 0n) + deltaMicros;
|
|
65739
|
+
if (next > 0n) this.sessionHolds.set(sessionId, next);
|
|
65740
|
+
else this.sessionHolds.delete(sessionId);
|
|
65575
65741
|
}
|
|
65576
65742
|
/**
|
|
65577
65743
|
* Stop the cleanup interval.
|
|
@@ -96274,6 +96440,19 @@ async function startProxy(options) {
|
|
|
96274
96440
|
}
|
|
96275
96441
|
const account = walletKey ? privateKeyToAccount(walletKey) : void 0;
|
|
96276
96442
|
const x402 = authMode === "wallet" ? new x402Client() : void 0;
|
|
96443
|
+
x402?.onBeforePaymentCreation(async (context) => {
|
|
96444
|
+
const spend = paymentStore.getStore()?.sessionSpend;
|
|
96445
|
+
if (!spend?.isStrict) return;
|
|
96446
|
+
const quote = parseQuotedAmountMicros(context.selectedRequirements);
|
|
96447
|
+
if (quote === void 0) {
|
|
96448
|
+
throw new CostCapError("ClawRouter cost cap: unreadable payment amount in the 402 quote");
|
|
96449
|
+
}
|
|
96450
|
+
if (!spend.coverPayment(quote)) {
|
|
96451
|
+
throw new CostCapError(
|
|
96452
|
+
`ClawRouter cost cap exceeded: signing $${microsToUsd(quote).toFixed(6)} would push this session past its maxCostPerRun limit`
|
|
96453
|
+
);
|
|
96454
|
+
}
|
|
96455
|
+
});
|
|
96277
96456
|
if (x402 && account) {
|
|
96278
96457
|
const evmPublicClient = createPublicClient({ chain: base, transport: http() });
|
|
96279
96458
|
const evmSigner = toClientEvmSigner(account, evmPublicClient);
|
|
@@ -96316,6 +96495,11 @@ async function startProxy(options) {
|
|
|
96316
96495
|
if (store) store.amountUsd = amountUsd;
|
|
96317
96496
|
console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
|
|
96318
96497
|
});
|
|
96498
|
+
x402?.onAfterPaymentCreation(async (context) => {
|
|
96499
|
+
const spend = paymentStore.getStore()?.sessionSpend;
|
|
96500
|
+
const amount = parseQuotedAmountMicros(context.selectedRequirements);
|
|
96501
|
+
if (spend && amount !== void 0) spend.recordSigned(amount);
|
|
96502
|
+
});
|
|
96319
96503
|
const payFetch = authMode === "api-key" ? withSpendPolicy(
|
|
96320
96504
|
createApiKeyFetch(apiKey, fetch, apiBase),
|
|
96321
96505
|
options.spendControl ?? getSharedSpendControl(),
|
|
@@ -96334,7 +96518,10 @@ async function startProxy(options) {
|
|
|
96334
96518
|
// Only the wallet rail settles per call. The API-key rail is billed
|
|
96335
96519
|
// server-side against account credit, so there is no per-request
|
|
96336
96520
|
// settlement for an observer to report.
|
|
96337
|
-
onPayment: options.onPayment
|
|
96521
|
+
onPayment: options.onPayment,
|
|
96522
|
+
// A 402 answer to a signed payment means the gateway declined it and
|
|
96523
|
+
// nothing settled — take it back off the session's budget.
|
|
96524
|
+
onPaymentDeclined: () => paymentStore.getStore()?.sessionSpend?.voidLastSigned()
|
|
96338
96525
|
});
|
|
96339
96526
|
let balanceMonitor;
|
|
96340
96527
|
if (options._balanceMonitorOverride) {
|
|
@@ -97618,6 +97805,17 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
97618
97805
|
return { success: true, response };
|
|
97619
97806
|
} catch (err) {
|
|
97620
97807
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
97808
|
+
if (err instanceof CostCapError) {
|
|
97809
|
+
return {
|
|
97810
|
+
success: false,
|
|
97811
|
+
errorBody: JSON.stringify({
|
|
97812
|
+
error: { message: errorMsg, type: "cost_cap_exceeded", code: "cost_cap_exceeded" }
|
|
97813
|
+
}),
|
|
97814
|
+
errorStatus: 429,
|
|
97815
|
+
isProviderError: false,
|
|
97816
|
+
errorCategory: "payment_error"
|
|
97817
|
+
};
|
|
97818
|
+
}
|
|
97621
97819
|
if (err instanceof SpendPolicyError) {
|
|
97622
97820
|
return {
|
|
97623
97821
|
success: false,
|
|
@@ -98697,14 +98895,23 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
98697
98895
|
}
|
|
98698
98896
|
}
|
|
98699
98897
|
}
|
|
98898
|
+
let sessionSpend;
|
|
98700
98899
|
if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "strict") {
|
|
98701
|
-
const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
|
|
98702
98900
|
const thisReqEstStr = estimatedCostMicros !== void 0 ? estimatedCostMicros.toString() : modelId ? estimateAmount(modelId, body.length, maxTokens) : void 0;
|
|
98703
|
-
const
|
|
98704
|
-
|
|
98705
|
-
|
|
98901
|
+
const admission = SessionSpend.admit(
|
|
98902
|
+
sessionStore,
|
|
98903
|
+
effectiveSessionId,
|
|
98904
|
+
usdToMicros(options.maxCostPerRunUsd),
|
|
98905
|
+
parseMicros(thisReqEstStr) ?? 0n
|
|
98906
|
+
);
|
|
98907
|
+
if (!admission.admitted) {
|
|
98908
|
+
const spentUsd = microsToUsd(admission.spentMicros);
|
|
98909
|
+
const heldUsd = microsToUsd(admission.heldMicros);
|
|
98910
|
+
const estUsd = microsToUsd(admission.estimateMicros);
|
|
98911
|
+
const projectedCostUsd = spentUsd + heldUsd + estUsd;
|
|
98912
|
+
const breakdown = `spent $${spentUsd.toFixed(4)}${heldUsd > 0 ? ` + in flight $${heldUsd.toFixed(4)}` : ""} + est $${estUsd.toFixed(4)}`;
|
|
98706
98913
|
console.log(
|
|
98707
|
-
`[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (
|
|
98914
|
+
`[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (${breakdown}) > $${options.maxCostPerRunUsd} limit`
|
|
98708
98915
|
);
|
|
98709
98916
|
res.writeHead(429, {
|
|
98710
98917
|
"Content-Type": "application/json",
|
|
@@ -98713,7 +98920,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
98713
98920
|
res.end(
|
|
98714
98921
|
JSON.stringify({
|
|
98715
98922
|
error: {
|
|
98716
|
-
message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (
|
|
98923
|
+
message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (${breakdown}) would exceed limit $${options.maxCostPerRunUsd}`,
|
|
98717
98924
|
type: "cost_cap_exceeded",
|
|
98718
98925
|
code: "cost_cap_exceeded"
|
|
98719
98926
|
}
|
|
@@ -98722,7 +98929,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
98722
98929
|
deduplicator.removeInflight(dedupKey);
|
|
98723
98930
|
return;
|
|
98724
98931
|
}
|
|
98932
|
+
sessionSpend = admission.spend;
|
|
98933
|
+
} else if (options.maxCostPerRunUsd && effectiveSessionId) {
|
|
98934
|
+
sessionSpend = SessionSpend.track(sessionStore, effectiveSessionId);
|
|
98725
98935
|
}
|
|
98936
|
+
const requestStore = paymentStore.getStore();
|
|
98937
|
+
if (requestStore) requestStore.sessionSpend = sessionSpend;
|
|
98726
98938
|
if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
|
|
98727
98939
|
const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
|
|
98728
98940
|
const remainingUsd = options.maxCostPerRunUsd - runCostUsd;
|
|
@@ -99118,11 +99330,9 @@ data: [DONE]
|
|
|
99118
99330
|
actualModelUsed = tryModel;
|
|
99119
99331
|
recordOutcome(tryModel, requestKind, true);
|
|
99120
99332
|
console.log(`[ClawRouter] Success with model: ${tryModel}`);
|
|
99121
|
-
if (
|
|
99122
|
-
const costEst = estimateAmount(tryModel, body.length, maxTokens);
|
|
99123
|
-
if (costEst)
|
|
99124
|
-
sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
|
|
99125
|
-
}
|
|
99333
|
+
if (sessionSpend && !FREE_MODELS.has(tryModel)) {
|
|
99334
|
+
const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
|
|
99335
|
+
if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
|
|
99126
99336
|
}
|
|
99127
99337
|
break;
|
|
99128
99338
|
}
|
|
@@ -99196,11 +99406,9 @@ data: [DONE]
|
|
|
99196
99406
|
upstream = retryResult.response;
|
|
99197
99407
|
actualModelUsed = tryModel;
|
|
99198
99408
|
console.log(`[ClawRouter] Explicit-pin retry succeeded for: ${tryModel}`);
|
|
99199
|
-
if (
|
|
99200
|
-
const costEst = estimateAmount(tryModel, body.length, maxTokens);
|
|
99201
|
-
if (costEst)
|
|
99202
|
-
sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
|
|
99203
|
-
}
|
|
99409
|
+
if (sessionSpend && !FREE_MODELS.has(tryModel)) {
|
|
99410
|
+
const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
|
|
99411
|
+
if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
|
|
99204
99412
|
}
|
|
99205
99413
|
break;
|
|
99206
99414
|
}
|
|
@@ -99267,11 +99475,9 @@ data: [DONE]
|
|
|
99267
99475
|
upstream = retryResult.response;
|
|
99268
99476
|
actualModelUsed = tryModel;
|
|
99269
99477
|
console.log(`[ClawRouter] Rate-limit retry succeeded for: ${tryModel}`);
|
|
99270
|
-
if (
|
|
99271
|
-
const costEst = estimateAmount(tryModel, body.length, maxTokens);
|
|
99272
|
-
if (costEst)
|
|
99273
|
-
sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
|
|
99274
|
-
}
|
|
99478
|
+
if (sessionSpend && !FREE_MODELS.has(tryModel)) {
|
|
99479
|
+
const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
|
|
99480
|
+
if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
|
|
99275
99481
|
}
|
|
99276
99482
|
break;
|
|
99277
99483
|
}
|
|
@@ -99365,6 +99571,7 @@ data: [DONE]
|
|
|
99365
99571
|
);
|
|
99366
99572
|
const rawErrBody = lastError?.body || structuredMessage;
|
|
99367
99573
|
const errStatus = lastError?.status || 502;
|
|
99574
|
+
const costCapRefusal = errStatus === 429 && rawErrBody.includes('"cost_cap_exceeded"');
|
|
99368
99575
|
const transformedErr = transformPaymentError(rawErrBody);
|
|
99369
99576
|
requestHadError = true;
|
|
99370
99577
|
if (headersSentEarly) {
|
|
@@ -99401,7 +99608,8 @@ data: [DONE]
|
|
|
99401
99608
|
"Content-Type": "application/json",
|
|
99402
99609
|
"x-context-used-kb": String(originalContextSizeKB),
|
|
99403
99610
|
"x-context-limit-kb": String(CONTEXT_LIMIT_KB),
|
|
99404
|
-
...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {}
|
|
99611
|
+
...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {},
|
|
99612
|
+
...costCapRefusal ? { "X-ClawRouter-Cost-Cap-Exceeded": "1" } : {}
|
|
99405
99613
|
});
|
|
99406
99614
|
res.end(transformedErr);
|
|
99407
99615
|
deduplicator.complete(dedupKey, {
|
|
@@ -99835,6 +100043,8 @@ data: [DONE]
|
|
|
99835
100043
|
throw new Error(`Request timed out after ${timeoutMs}ms`, { cause: err });
|
|
99836
100044
|
}
|
|
99837
100045
|
throw err;
|
|
100046
|
+
} finally {
|
|
100047
|
+
sessionSpend?.release();
|
|
99838
100048
|
}
|
|
99839
100049
|
const logModel = routingDecision?.model ?? modelId;
|
|
99840
100050
|
if (logModel) {
|
|
@@ -99921,6 +100131,7 @@ var init_proxy = __esm({
|
|
|
99921
100131
|
init_auth();
|
|
99922
100132
|
init_api_key();
|
|
99923
100133
|
init_spend_control();
|
|
100134
|
+
init_session_spend();
|
|
99924
100135
|
init_compression();
|
|
99925
100136
|
init_errors5();
|
|
99926
100137
|
init_version4();
|
|
@@ -122622,7 +122833,7 @@ var init_client5 = __esm({
|
|
|
122622
122833
|
function resolveSpendControl(deps) {
|
|
122623
122834
|
return deps?.spendControl ?? getSharedSpendControl();
|
|
122624
122835
|
}
|
|
122625
|
-
function
|
|
122836
|
+
function usdToMicros2(usd) {
|
|
122626
122837
|
return String(Math.ceil(usd * 1e6));
|
|
122627
122838
|
}
|
|
122628
122839
|
async function signUnderSpendPolicy(deps, quote, action, sign4) {
|
|
@@ -122888,7 +123099,7 @@ Re-call with confirm:true to sign and submit.`,
|
|
|
122888
123099
|
payTo: negRisk ? NEG_RISK_CTF_EXCHANGE_V2 : CTF_EXCHANGE_V2,
|
|
122889
123100
|
network: `eip155:${POLYGON_CHAIN_ID}`,
|
|
122890
123101
|
asset: PUSD_COLLATERAL,
|
|
122891
|
-
amount:
|
|
123102
|
+
amount: usdToMicros2(notional)
|
|
122892
123103
|
};
|
|
122893
123104
|
let r2;
|
|
122894
123105
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 9 Free AI Models, Zero Cost: How BlockRun Gives Developers Top-Tier LLMs for Nothing
|
|
2
2
|
|
|
3
3
|
> **The count in this title is a snapshot, and so is every model name below.**
|
|
4
|
-
> It was 9 when this was written; the published free tier is <!-- br:models.free -->
|
|
4
|
+
> It was 9 when this was written; the published free tier is <!-- br:models.free -->7<!-- /br:models.free --> today, and _none_ of the
|
|
5
5
|
> nine originals is still in it. Free hosting is volatile — NVIDIA retired four
|
|
6
6
|
> of the five visible free models in a single sweep on 2026-08-30 — so the tier
|
|
7
7
|
> gets rebuilt rather than topped up. The URL keeps its original slug so existing
|
|
@@ -23,7 +23,7 @@ BlockRun's answer: just take them.
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
## The Lineup: <!-- br:models.free -->
|
|
26
|
+
## The Lineup: <!-- br:models.free -->7<!-- /br:models.free --> Models, $0.00
|
|
27
27
|
|
|
28
28
|
Through [ClawRouter](https://github.com/BlockRunAI/ClawRouter) — BlockRun's local AI routing proxy — you get zero-cost access to the following (checked against the live catalog 2026-09-29):
|
|
29
29
|
|
|
@@ -236,13 +236,13 @@ Look at the cost trajectory over the past three years:
|
|
|
236
236
|
- **2023:** GPT-4 dominates alone at $30/$60 per M tokens
|
|
237
237
|
- **2024:** Open-source models surge, prices halve repeatedly
|
|
238
238
|
- **2025:** DeepSeek, Qwen push top-tier inference below $1/M
|
|
239
|
-
- **2026:** BlockRun offers <!-- br:models.free -->
|
|
239
|
+
- **2026:** BlockRun offers <!-- br:models.free -->7<!-- /br:models.free --> free models through a single API
|
|
240
240
|
|
|
241
241
|
**A free tier that gets rebuilt rather than retired isn't just a product feature — it's a signal.** Baseline AI capability is becoming infrastructure. Like internet bandwidth before it, the cost of "good enough" AI inference is converging toward zero.
|
|
242
242
|
|
|
243
243
|
BlockRun and ClawRouter exist to be the **routing layer** in this transition: not locked to any single provider, not bound to any single model, always giving developers the lowest-cost path to the right capability.
|
|
244
244
|
|
|
245
|
-
Today it's <!-- br:models.free -->
|
|
245
|
+
Today it's <!-- br:models.free -->7<!-- /br:models.free --> free models, and they are not the same seven as last month. Tomorrow it could be 50. Prices will only drop. Capabilities will only improve. The names will keep churning.
|
|
246
246
|
|
|
247
247
|
**The one constant: your code doesn't need to change.**
|
|
248
248
|
|
|
@@ -257,7 +257,7 @@ clawrouter start
|
|
|
257
257
|
|
|
258
258
|
Point your `base_url` to `http://localhost:8402/v1`. That's the whole setup.
|
|
259
259
|
|
|
260
|
-
<!-- br:models.free -->
|
|
260
|
+
<!-- br:models.free -->7<!-- /br:models.free --> free models. Up to 1M context. Unlimited calls. Zero cost.
|
|
261
261
|
|
|
262
262
|
Go build something.
|
|
263
263
|
|
|
@@ -50,7 +50,7 @@ This is where you're paying for real value:
|
|
|
50
50
|
|
|
51
51
|
## The Solution: ClawRouter
|
|
52
52
|
|
|
53
|
-
[ClawRouter](https://github.com/BlockRunAI/ClawRouter) is an open-source local proxy that sits between your app and <!-- br:models.chatVisible -->
|
|
53
|
+
[ClawRouter](https://github.com/BlockRunAI/ClawRouter) is an open-source local proxy that sits between your app and <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> AI models. It saves you money in three ways: **smart routing**, **token optimization**, and **response caching**.
|
|
54
54
|
|
|
55
55
|
```
|
|
56
56
|
┌─────────────┐ ┌──────────────────────────────┐ ┌──────────────────┐
|
|
@@ -31,7 +31,7 @@ This is exactly the problem [ClawRouter](https://github.com/BlockRunAI/ClawRoute
|
|
|
31
31
|
|
|
32
32
|
## ClawRouter: Smart Routing for Agents
|
|
33
33
|
|
|
34
|
-
ClawRouter is an open-source local proxy that sits between your agent and <!-- br:models.chatVisible -->
|
|
34
|
+
ClawRouter is an open-source local proxy that sits between your agent and <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> LLM models across 9 providers. It analyzes every request across 15 dimensions and routes it to the cheapest model that can handle it — in under 1ms, entirely locally.
|
|
35
35
|
|
|
36
36
|
```
|
|
37
37
|
Your Agent → ClawRouter (localhost:8402) → Best model for the job
|
|
@@ -74,12 +74,12 @@ Then point your agent at `http://localhost:8402/v1/` with any OpenAI-compatible
|
|
|
74
74
|
|
|
75
75
|
- **No API keys to manage** — wallet-based cryptographic auth
|
|
76
76
|
- **No subscriptions** — pay per request in USDC (Base or Solana)
|
|
77
|
-
- **No vendor lock-in** — <!-- br:models.chatVisible -->
|
|
77
|
+
- **No vendor lock-in** — <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, switch anytime
|
|
78
78
|
- **You control your wallet** — non-custodial, funds never held by a third party
|
|
79
79
|
- **Budget caps** — set a max spend per session, ClawRouter gracefully downgrades when budget runs low
|
|
80
80
|
- **Token compression** — 7-layer pipeline reduces token costs by 7–40% before they hit any provider
|
|
81
81
|
- **Response caching** — identical requests within 10 minutes cost $0
|
|
82
|
-
- **<!-- br:models.free -->
|
|
82
|
+
- **<!-- br:models.free -->7<!-- /br:models.free --> free models** — for tasks that don't need a paid model at all
|
|
83
83
|
|
|
84
84
|
### Routing Profiles
|
|
85
85
|
|
|
@@ -222,7 +222,7 @@ OpenRouter doesn't always pass through provider-specific features correctly. Ima
|
|
|
222
222
|
|
|
223
223
|
**Direct provider routing.** ClawRouter routes through BlockRun's API directly to providers — not through a second aggregator. One hop, not two. Provider-specific features work because there's no middleman translating them.
|
|
224
224
|
|
|
225
|
-

|
|
226
226
|
|
|
227
227
|
---
|
|
228
228
|
|
|
@@ -240,7 +240,7 @@ When new models launch, OpenRouter's catalog lags. Users configure a model that
|
|
|
240
240
|
|
|
241
241
|
### How ClawRouter Solves This
|
|
242
242
|
|
|
243
|
-
ClawRouter maintains a curated catalog of <!-- br:models.chatVisible -->
|
|
243
|
+
ClawRouter maintains a curated catalog of <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models across 9 providers (including <!-- br:models.free -->7<!-- /br:models.free --> free models), updated with each release. Delisted models have automatic redirect aliases:
|
|
244
244
|
|
|
245
245
|
```typescript
|
|
246
246
|
// Delisted models redirect automatically
|
|
@@ -268,7 +268,7 @@ No silent drops. No stale catalog. Models are benchmarked for speed, quality, an
|
|
|
268
268
|
| **Rate limits** | Per-key, shared | Per-wallet, independent |
|
|
269
269
|
| **Vision support** | Images sometimes dropped | Auto-detected, vision-only fallback |
|
|
270
270
|
| **Tool calling** | Silent failures with some models | Flag-based filtering, guaranteed support |
|
|
271
|
-
| **Model catalog** | Laggy, silent drops | Curated <!-- br:models.chatVisible -->
|
|
271
|
+
| **Model catalog** | Laggy, silent drops | Curated <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, redirect aliases |
|
|
272
272
|
| **Budget control** | Monthly invoice | Per-session cap (`maxCostPerRun`) |
|
|
273
273
|
| **Setup** | Create account, paste key | Agent generates wallet, auto-configured |
|
|
274
274
|
| **Average cost** | $25/M tokens (Opus direct) | auto-routed = **<!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% savings** |
|
package/docs/configuration.md
CHANGED
|
@@ -489,9 +489,15 @@ plugins:
|
|
|
489
489
|
# graceful (default): when budget runs low, ClawRouter automatically downgrades
|
|
490
490
|
# to cheaper models (premium → auto → eco → free). Tasks keep running.
|
|
491
491
|
# Only returns an error if no model can serve the request at all.
|
|
492
|
+
# Spend is the amount each signed payment authorized, counted when it is
|
|
493
|
+
# signed — a payment whose response is lost still counts.
|
|
492
494
|
#
|
|
493
495
|
# strict: immediately returns 429 (X-ClawRouter-Cost-Cap-Exceeded: 1) once
|
|
494
496
|
# the session spend reaches the cap. Use when you need a hard budget ceiling.
|
|
497
|
+
# Requests in flight hold their estimated cost, so concurrent calls cannot
|
|
498
|
+
# all pass on the same spent total; every signed payment counts at once,
|
|
499
|
+
# even one whose response is lost; and each payment (including retries and
|
|
500
|
+
# fallbacks) is checked against the cap before it is signed.
|
|
495
501
|
maxCostPerRunMode: graceful # or: strict
|
|
496
502
|
|
|
497
503
|
# Note: image generation endpoints (/v1/images/generations) bypass maxCostPerRun.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/clawrouter",
|
|
3
|
-
"version": "0.12.
|
|
4
|
-
"description": "Smart LLM router — save 84% on inference costs.
|
|
3
|
+
"version": "0.12.284",
|
|
4
|
+
"description": "Smart LLM router — save 84% on inference costs. 88 models (7 free). Pay by credit card (API key) or USDC (x402 wallet).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clawrouter
|
|
3
|
-
description: Hosted-gateway LLM router — save 84% on inference costs. A local proxy that forwards each request to the blockrun.ai gateway, which routes to the cheapest capable model across
|
|
3
|
+
description: Hosted-gateway LLM router — save 84% on inference costs. A local proxy that forwards each request to the blockrun.ai gateway, which routes to the cheapest capable model across 88 models from OpenAI, Anthropic, Google, DeepSeek, xAI, Z.AI, and more. 7 free open-weight models included. Also exposes realtime market data (global stocks, crypto, FX, commodities), Twitter/X intelligence, prediction-market data across Polymarket, Kalshi, Limitless, Opinion, Predict.Fun, dFlow + UMA oracle resolution + wallet identity & clustering, phone-number intelligence (carrier + SIM-swap fraud detection) plus AI-powered outbound voice calls (Twilio + Bland.ai), AND the Surf unified crypto data API (84 endpoints — CEX/DEX, on-chain SQL over 80+ ClickHouse tables, 100M+ labeled wallets, prediction markets, social/CT mindshare, news, VC fund intel) as built-in agent tools. Not a local-inference tool — prompts are sent to the blockrun.ai gateway.
|
|
4
4
|
triggers:
|
|
5
5
|
- "clawrouter"
|
|
6
6
|
- "claw router"
|
|
@@ -53,7 +53,7 @@ metadata:
|
|
|
53
53
|
|
|
54
54
|
# ClawRouter
|
|
55
55
|
|
|
56
|
-
Hosted-gateway LLM router that saves <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% on inference costs by forwarding each request to the blockrun.ai gateway, which picks the cheapest model capable of handling it across <!-- br:models.chatVisible -->
|
|
56
|
+
Hosted-gateway LLM router that saves <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% on inference costs by forwarding each request to the blockrun.ai gateway, which picks the cheapest model capable of handling it across <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models from 9 providers (<!-- br:models.free -->7<!-- /br:models.free --> free open-weight models). All billing flows through one BlockRun credential — either a USDC wallet paying x402 per call, or a BlockRun API key drawing on card-funded account credit. Either way you do not hold provider API keys.
|
|
57
57
|
|
|
58
58
|
**This is not a local-inference tool.** ClawRouter is a thin local proxy. Your prompts are sent over HTTPS to the blockrun.ai gateway for model execution. If your workload requires inference that never leaves your machine, use a local runtime like Ollama — ClawRouter is not the right tool for that use case.
|
|
59
59
|
|
|
@@ -147,7 +147,7 @@ it against a baseline nobody wrote down. See
|
|
|
147
147
|
|
|
148
148
|
## Available Models
|
|
149
149
|
|
|
150
|
-
<!-- br:models.chatVisible -->
|
|
150
|
+
<!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models including: gpt-6-astra [GPT-6 flagship, 1M ctx], gpt-6-sol, gpt-6-luna [fast, $0.10/$0.50], gpt-5.6-terra [balanced, stable default], gpt-5.6-sol [flagship], gpt-5.6-luna [cost-efficient], gpt-5.6-sol/terra/luna-pro [pro reasoning tiers], gpt-5.5, gpt-5.5-pro [max compute], chat-latest [ChatGPT Instant], gpt-5.4, gpt-5.1, gpt-4o, o3, claude-fable-5.1, claude-fable-5, claude-opus-5.5 [newest Opus, $4/$20], claude-opus-5 [1M ctx], claude-opus-4.8, claude-opus-4.7, claude-opus-4.5, claude-sonnet-5.5, claude-sonnet-5, claude-sonnet-4.6, gemini-3.1-pro, gemini-3.6-flash, gemini-3.5-flash-lite, gemini-2.5-flash, deepseek-v4-pro, deepseek-chat, grok-4.7, grok-4.6, grok-4.5, grok-4.3, grok-build-0.1, glm-5.3 [Z.AI flagship, 1M ctx], glm-5.3-flash [natively multimodal, 1M ctx — $0.15/$0.50], glm-5.2, minimax-m3, kimi-k3 [1M ctx flagship], kimi-k2.7, qwen3.7-max [Qwen flagship, 1M ctx], qwen3.7-plus, qwen3.7-flash, mimo [Xiaomi MiMo-V2.5 Pro, 1M ctx], and <!-- br:models.free -->7<!-- /br:models.free --> free open-weight models (nemotron-3.5-lightning [1M ctx, thinking mode — the free default], nemotron-3-ultra-550b [550B/55B MoE, 1M ctx — the largest free model], nemotron-3-nano-omni-30b-a3b-reasoning [256K ctx], llama-3.2-11b-vision [Meta Llama], north-mini-code [Cohere, coding, sub-second], laguna-xs-2.1 [Poolside, coding, ~161 tok/s]). The free tier is text-only — image turns route to a paid vision model, because the two free models the catalogs advertise as vision-capable fail a solid-colour probe on both chains and do so with an HTTP 200.
|
|
151
151
|
|
|
152
152
|
## Built-in Agent Tools
|
|
153
153
|
|