@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/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
- const entry = this.sessions.get(sessionId);
65573
- if (!entry) return 0;
65574
- return Number(entry.sessionCostMicros) / 1e6;
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 thisReqEstUsd = thisReqEstStr ? Number(thisReqEstStr) / 1e6 : 0;
98704
- const projectedCostUsd = runCostUsd + thisReqEstUsd;
98705
- if (projectedCostUsd > options.maxCostPerRunUsd) {
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)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) > $${options.maxCostPerRunUsd} limit`
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)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) would exceed limit $${options.maxCostPerRunUsd}`,
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 (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
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 (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
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 (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
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 usdToMicros(usd) {
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: usdToMicros(notional)
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 -->6<!-- /br:models.free --> today, and _none_ of the
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 -->6<!-- /br:models.free --> Models, $0.00
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 -->6<!-- /br:models.free --> free models through a single API
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 -->6<!-- /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.
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 -->6<!-- /br:models.free --> free models. Up to 1M context. Unlimited calls. Zero cost.
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 -->82<!-- /br:models.chatVisible --> AI models. It saves you money in three ways: **smart routing**, **token optimization**, and **response caching**.
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 -->82<!-- /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.
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 -->82<!-- /br:models.chatVisible --> models, switch anytime
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 -->6<!-- /br:models.free --> free models** — for tasks that don't need a paid model at all
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
- ![Guaranteed Feature Parity & Direct Connectivity — Three-panel diagram: Vision (image_url auto-detected → vision-capable models only), Tool Calling (toolCalling flag → agentic models only), Catalog (curated <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> models with automatic legacy-to-modern redirects). Direct provider routing means no dropped payloads.](./assets/clawrouter-feature-parity-direct-connectivity.png)
225
+ ![Guaranteed Feature Parity & Direct Connectivity — Three-panel diagram: Vision (image_url auto-detected → vision-capable models only), Tool Calling (toolCalling flag → agentic models only), Catalog (curated <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models with automatic legacy-to-modern redirects). Direct provider routing means no dropped payloads.](./assets/clawrouter-feature-parity-direct-connectivity.png)
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 -->82<!-- /br:models.chatVisible --> models across 9 providers (including <!-- br:models.free -->6<!-- /br:models.free --> free models), updated with each release. Delisted models have automatic redirect aliases:
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 -->82<!-- /br:models.chatVisible --> models, redirect aliases |
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** |
@@ -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.282",
4
- "description": "Smart LLM router — save 84% on inference costs. 82 models (6 free). Pay by credit card (API key) or USDC (x402 wallet).",
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 82 models from OpenAI, Anthropic, Google, DeepSeek, xAI, Z.AI, and more. 6 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.
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 -->82<!-- /br:models.chatVisible --> models from 9 providers (<!-- br:models.free -->6<!-- /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.
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 -->82<!-- /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 -->6<!-- /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.
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