@blockrun/clawrouter 0.12.282 → 0.12.283
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 +20 -20
- package/dist/cli.js +239 -28
- 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/README.md
CHANGED
|
@@ -9,11 +9,11 @@ Agents can only sign transactions.<br><br>
|
|
|
9
9
|
<strong>ClawRouter is the only LLM router that lets agents operate independently.</strong><br><br>
|
|
10
10
|
<em>Pay however you like: <strong>credit card</strong> — sign up at <a href="https://user.blockrun.ai"><strong>user.blockrun.ai</strong></a>, top up, mint an API key<br>
|
|
11
11
|
or <strong>USDC</strong> on Solana or Base (x402 wallet, no account at all).<br>
|
|
12
|
-
Or neither — <!-- br:models.free -->
|
|
12
|
+
Or neither — <!-- br:models.free -->7<!-- /br:models.free --> models are free, with no signup, no key and no wallet.</em></p>
|
|
13
13
|
|
|
14
14
|
<br>
|
|
15
15
|
|
|
16
|
-
<img src="https://img.shields.io/badge/🆓
|
|
16
|
+
<img src="https://img.shields.io/badge/🆓_7_Free_Models-success?style=for-the-badge" alt="7 free models">
|
|
17
17
|
<img src="https://img.shields.io/badge/🤖_Agent--Native-black?style=for-the-badge" alt="Agent native">
|
|
18
18
|
<img src="https://img.shields.io/badge/💳_Card_or_USDC-blue?style=for-the-badge" alt="Card or USDC">
|
|
19
19
|
<img src="https://img.shields.io/badge/⚡_Local_Routing-yellow?style=for-the-badge" alt="Local routing">
|
|
@@ -36,7 +36,7 @@ Or neither — <!-- br:models.free -->6<!-- /br:models.free --> models are free,
|
|
|
36
36
|
|
|
37
37
|
</div>
|
|
38
38
|
|
|
39
|
-
> **ClawRouter** is an open-source smart LLM router that reduces AI API costs by up to <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->%. It analyzes each request across <!-- br:clawrouter.dimensions -->15<!-- /br:clawrouter.dimensions --> dimensions and routes to the cheapest capable model in under 1ms, entirely locally. It is the only LLM router that an autonomous agent can pay for by itself — a wallet signature is the account, and USDC micropayments over the x402 protocol are the billing — and it takes an ordinary credit card too: sign up at [user.blockrun.ai](https://user.blockrun.ai), top up, and run the same router on an API key. <!-- br:models.chatVisible -->
|
|
39
|
+
> **ClawRouter** is an open-source smart LLM router that reduces AI API costs by up to <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->%. It analyzes each request across <!-- br:clawrouter.dimensions -->15<!-- /br:clawrouter.dimensions --> dimensions and routes to the cheapest capable model in under 1ms, entirely locally. It is the only LLM router that an autonomous agent can pay for by itself — a wallet signature is the account, and USDC micropayments over the x402 protocol are the billing — and it takes an ordinary credit card too: sign up at [user.blockrun.ai](https://user.blockrun.ai), top up, and run the same router on an API key. <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models from OpenAI, Anthropic, Google, xAI, DeepSeek, and more. MIT licensed.
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
@@ -48,7 +48,7 @@ Not a seat, not a subscription, not a monthly minimum you keep paying while the
|
|
|
48
48
|
|
|
49
49
|
| | |
|
|
50
50
|
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
-
| **Every LLM** | <!-- br:models.chatVisible -->
|
|
51
|
+
| **Every LLM** | <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> chat models across 9 providers, behind one endpoint and one balance |
|
|
52
52
|
| **Every tool** | <!-- br:models.image -->12<!-- /br:models.image --> image models, <!-- br:models.video -->8<!-- /br:models.video --> video models, music, <!-- br:models.speech -->5<!-- /br:models.speech --> voices, img2img editing, and outbound phone calls that come back as transcripts |
|
|
53
53
|
| **Every data source** | web, news and neural search; prediction markets; live crypto and equity quotes; on-chain SQL over 100M+ labeled wallets; DEX routing; RPC across <!-- br:chains.rpc -->40<!-- /br:chains.rpc --> chains |
|
|
54
54
|
| **Best value per dollar** | <!-- br:clawrouter.dimensions -->15<!-- /br:clawrouter.dimensions -->-dimension local routing sends each request to the cheapest model that can actually do it — <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% below pinning Claude Opus 5 on the same traffic, computed from a published workload mix rather than estimated |
|
|
@@ -65,7 +65,7 @@ Every other LLM router was built for **human developers** — create an account,
|
|
|
65
65
|
|
|
66
66
|
ClawRouter is built for the agent-first world:
|
|
67
67
|
|
|
68
|
-
- **Starts at $0** — <!-- br:models.free -->
|
|
68
|
+
- **Starts at $0** — <!-- br:models.free -->7<!-- /br:models.free --> open-weight models are free forever (incl. a 1M-context reasoner and two sub-second coders). Image turns route to paid models — see the free-tier note below.
|
|
69
69
|
- **No accounts needed** — a wallet is generated locally, no signup
|
|
70
70
|
- **No API keys needed** — your wallet signature IS authentication
|
|
71
71
|
- **No model selection** — <!-- br:clawrouter.dimensions -->15<!-- /br:clawrouter.dimensions -->-dimension scoring + constraint-first ranking ([router-core](https://github.com/BlockRunAI/router-core)) picks the right model automatically
|
|
@@ -76,7 +76,7 @@ This is the stack that lets agents operate autonomously: **x402 + USDC + local r
|
|
|
76
76
|
|
|
77
77
|
### …and if you are a human, not an agent
|
|
78
78
|
|
|
79
|
-
Every "no X needed" above is a capability, not a restriction. **ClawRouter takes a credit card too.** Sign up at **[user.blockrun.ai](https://user.blockrun.ai)**, top up, mint an API key, and run `clawrouter login brk_live_…`. Same router, same <!-- br:models.chatVisible -->
|
|
79
|
+
Every "no X needed" above is a capability, not a restriction. **ClawRouter takes a credit card too.** Sign up at **[user.blockrun.ai](https://user.blockrun.ai)**, top up, mint an API key, and run `clawrouter login brk_live_…`. Same router, same <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, same local routing — calls go to `api.blockrun.ai` on a bearer token and bill your account credit. No crypto, no gas, no seed phrase.
|
|
80
80
|
|
|
81
81
|
| You are | Pay with | Setup |
|
|
82
82
|
| ------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
@@ -92,8 +92,8 @@ Full walkthrough: [Paying with a credit card](#paying-with-a-credit-card-api-key
|
|
|
92
92
|
|
|
93
93
|
| | OpenRouter | LiteLLM | Martian | Portkey | **ClawRouter** |
|
|
94
94
|
| ---------------- | ----------------- | ---------------- | ----------------- | ----------------- | ---------------------------------------------------------------------- |
|
|
95
|
-
| **Models** | 200+ | 100+ | Smart routing | Gateway | **<!-- br:models.chatVisible -->
|
|
96
|
-
| **Free tier** | Rate-limited | BYO keys | No | No | **<!-- br:models.free -->
|
|
95
|
+
| **Models** | 200+ | 100+ | Smart routing | Gateway | **<!-- br:models.chatVisible -->88<!-- /br:models.chatVisible -->** |
|
|
96
|
+
| **Free tier** | Rate-limited | BYO keys | No | No | **<!-- br:models.free -->7<!-- /br:models.free --> models, no signup** |
|
|
97
97
|
| **Routing** | Manual selection | Manual selection | Smart (closed) | Observability | **Smart (open source)** |
|
|
98
98
|
| **Auth** | Account + API key | Your API keys | Account + API key | Account + API key | **Wallet signature _or_ API key** |
|
|
99
99
|
| **Payment** | Credit card | BYO keys | Credit card | $49-499/mo | **USDC per-request _or_ card credit** |
|
|
@@ -109,7 +109,7 @@ Full walkthrough: [Paying with a credit card](#paying-with-a-credit-card-api-key
|
|
|
109
109
|
|
|
110
110
|
## Quick Start
|
|
111
111
|
|
|
112
|
-
> **No wallet? <!-- br:models.free -->
|
|
112
|
+
> **No wallet? <!-- br:models.free -->7<!-- /br:models.free --> models work free out of the box.** Install, run, and pin `free/nemotron-3.5-lightning` (or any of the <!-- br:models.free -->7<!-- /br:models.free -->) — no crypto, no signup, no balance required. Add a credit card or USDC later when you want paid models.
|
|
113
113
|
|
|
114
114
|
**Two ways to pay for the paid ones — pick one, the rest of ClawRouter is identical:**
|
|
115
115
|
|
|
@@ -127,7 +127,7 @@ is all it takes to switch and `clawrouter logout` is all it takes to switch back
|
|
|
127
127
|
|
|
128
128
|
> **Where do I sign up?** [**user.blockrun.ai**](https://user.blockrun.ai) — sign in with
|
|
129
129
|
> Google, add credit with a card under **Credits**, mint a key under **API Keys**. That
|
|
130
|
-
> one key covers all <!-- br:models.chatVisible -->
|
|
130
|
+
> one key covers all <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> chat models
|
|
131
131
|
> plus images, video, speech and the live-data APIs. No wallet, no crypto, no seed phrase.
|
|
132
132
|
|
|
133
133
|
### Desktop control plane (macOS)
|
|
@@ -171,7 +171,7 @@ openclaw gateway restart
|
|
|
171
171
|
|
|
172
172
|
> **Using Claude Code?** Check out [BRCC](https://github.com/BlockRunAI/brcc) — it's purpose-built for Claude Code with the same smart routing and x402 payments.
|
|
173
173
|
>
|
|
174
|
-
> **Using NousResearch Hermes?** See [ClawRouter-Hermes](https://github.com/BlockRunAI/ClawRouter-Hermes) — a Python plugin that wires Hermes into the ClawRouter proxy. Same wallet, same <!-- br:models.chatVisible -->
|
|
174
|
+
> **Using NousResearch Hermes?** See [ClawRouter-Hermes](https://github.com/BlockRunAI/ClawRouter-Hermes) — a Python plugin that wires Hermes into the ClawRouter proxy. Same wallet, same <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, same x402 USDC settlement on Solana & Base.
|
|
175
175
|
|
|
176
176
|
No OpenClaw required. ClawRouter runs as a local proxy on port 8402.
|
|
177
177
|
|
|
@@ -182,7 +182,7 @@ npx @blockrun/clawrouter
|
|
|
182
182
|
```
|
|
183
183
|
|
|
184
184
|
**2. Fund your wallet** — optional, skip for free tier
|
|
185
|
-
Your wallet address is printed on first run. For paid models, send a few USDC on Solana (the default chain) or Base — $5 covers thousands of requests. Prefer a card? [Sign up at user.blockrun.ai](https://user.blockrun.ai) and run `clawrouter login brk_live_…` instead. To stay at $0, pin any of the <!-- br:models.free -->
|
|
185
|
+
Your wallet address is printed on first run. For paid models, send a few USDC on Solana (the default chain) or Base — $5 covers thousands of requests. Prefer a card? [Sign up at user.blockrun.ai](https://user.blockrun.ai) and run `clawrouter login brk_live_…` instead. To stay at $0, pin any of the <!-- br:models.free -->7<!-- /br:models.free --> free models (e.g. `free/nemotron-3.5-lightning`) or use `/model free` inside OpenClaw.
|
|
186
186
|
|
|
187
187
|
**3. Point your client at `http://localhost:8402`**
|
|
188
188
|
|
|
@@ -485,7 +485,7 @@ No Surf account, no API key — settles directly to Surf's Base treasury in USDC
|
|
|
485
485
|
|
|
486
486
|
## Models & Pricing
|
|
487
487
|
|
|
488
|
-
<!-- br:models.chatVisible -->
|
|
488
|
+
<!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models across 9 providers, one wallet. **<!-- br:models.free -->7<!-- /br:models.free --> models are $0 — paid models start at fractions of a cent.**
|
|
489
489
|
|
|
490
490
|
> **💡 "Cost per request"** = estimated cost for a typical chat message (~500 input + 500 output tokens). Paid requests also carry a flat **$0.001/tx settlement fee** (covers on-chain gas; already included in the price the gateway quotes). Free models never pay it.
|
|
491
491
|
|
|
@@ -585,7 +585,7 @@ No Surf account, no API key — settles directly to Surf's Base treasury in USDC
|
|
|
585
585
|
| openai/gpt-5.4-pro | $30.00 | $180.00 | $0.1050 | 1M | reasoning, tools |
|
|
586
586
|
| openai/gpt-5.5-pro | $30.00 | $180.00 | $0.1050 | 1M | reasoning, vision, tools |
|
|
587
587
|
|
|
588
|
-
> **Free tier:** <!-- br:models.free -->
|
|
588
|
+
> **Free tier:** <!-- br:models.free -->7<!-- /br:models.free --> open-weight models cost nothing — `/model free` pins the free default (`nemotron-3.5-lightning`, with the other five as fallbacks), `/model eco` opens on it, or pick one directly (e.g., `/model lightning` for 1M-context reasoning, `/model north-mini` or `/model laguna` for fast coding).
|
|
589
589
|
>
|
|
590
590
|
> **No free vision.** Two free models are catalogued as vision-capable and neither reliably is: on a 64×64 solid-colour probe `nemotron-3-nano-omni` was right 1 of 4 times on Base and answered "white" for red on Solana, and `llama-3.2-11b-vision` replied "I'm unable to see the image" 3 of 3 while answering text fine. Both return HTTP 200, so a wrong answer arrives with no error to branch on. ClawRouter therefore does not flag them for vision, and requests carrying an `image_url` route to a paid vision model instead.
|
|
591
591
|
> **Best value:** `gpt-5-nano` and `gemini-2.5-flash-lite` deliver strong results at ~$0.0003/request.
|
|
@@ -728,7 +728,7 @@ chains, the response cache and the local spend controls.
|
|
|
728
728
|
- **Balance lives server-side.** There is no local balance to check: a call that
|
|
729
729
|
outruns your credit comes back as HTTP `402 insufficient_quota`, naming the
|
|
730
730
|
top-up page. Free models stay free and need no credit at all — all
|
|
731
|
-
<!-- br:models.free -->
|
|
731
|
+
<!-- br:models.free -->7<!-- /br:models.free --> are served on the API-key gateway too.
|
|
732
732
|
- **One key covers nearly everything.** Verified live against `api.blockrun.ai`
|
|
733
733
|
on 2026-09-05: chat completions (`/v1/chat/completions`), the Anthropic-shaped
|
|
734
734
|
`/v1/messages`, `/v1/models`, image generation, speech, video, and the partner
|
|
@@ -881,7 +881,7 @@ npm test
|
|
|
881
881
|
|
|
882
882
|
**The LLM router built for autonomous agents**
|
|
883
883
|
|
|
884
|
-
You're here. <!-- br:models.chatVisible -->
|
|
884
|
+
You're here. <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, local smart routing, and two ways to pay — a credit-card-funded API key, or x402 USDC from a wallet, which is what lets an agent operate independently.
|
|
885
885
|
|
|
886
886
|
`curl -fsSL https://blockrun.ai/ClawRouter-update | bash`
|
|
887
887
|
|
|
@@ -892,7 +892,7 @@ You're here. <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible --> mod
|
|
|
892
892
|
|
|
893
893
|
**BlockRun for Claude Code**
|
|
894
894
|
|
|
895
|
-
Run Claude Code with <!-- br:models.chatVisible -->
|
|
895
|
+
Run Claude Code with <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, no rate limits, no Anthropic account, no phone verification. Pay per request with USDC — your wallet is your identity.
|
|
896
896
|
|
|
897
897
|
`curl -fsSL https://blockrun.ai/brcc-install | bash`
|
|
898
898
|
|
|
@@ -905,7 +905,7 @@ Run Claude Code with <!-- br:models.chatVisible -->82<!-- /br:models.chatVisible
|
|
|
905
905
|
|
|
906
906
|
**ClawRouter for NousResearch Hermes**
|
|
907
907
|
|
|
908
|
-
Python plugin that wraps the ClawRouter proxy for `hermes-agent`. Same <!-- br:models.chatVisible -->
|
|
908
|
+
Python plugin that wraps the ClawRouter proxy for `hermes-agent`. Same <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> models, same x402 USDC payments on Solana & Base, native Hermes ergonomics.
|
|
909
909
|
|
|
910
910
|
`pip install hermes-plugin-clawrouter`
|
|
911
911
|
|
|
@@ -949,7 +949,7 @@ Python plugin that wraps the ClawRouter proxy for `hermes-agent`. Same <!-- br:m
|
|
|
949
949
|
|
|
950
950
|
### What is BlockRun?
|
|
951
951
|
|
|
952
|
-
BlockRun lets agents pay for the outcome — every LLM, tool and data source, best value per dollar. One endpoint and one balance covering <!-- br:models.chatVisible -->
|
|
952
|
+
BlockRun lets agents pay for the outcome — every LLM, tool and data source, best value per dollar. One endpoint and one balance covering <!-- br:models.chatVisible -->88<!-- /br:models.chatVisible --> chat models plus image, video, music and speech generation, search, market data and multi-chain RPC, billed per call with USDC over [x402](https://x402.org) or a credit-card-funded API key — no seat, no subscription, no minimum. ClawRouter is the open-source local router agents use to reach it.
|
|
953
953
|
|
|
954
954
|
### What is ClawRouter?
|
|
955
955
|
|
|
@@ -977,7 +977,7 @@ No. A wallet is the default because it is what lets an _agent_ pay for itself,
|
|
|
977
977
|
but you can sign up at **[user.blockrun.ai](https://user.blockrun.ai)**, top up
|
|
978
978
|
with a credit card, mint an API key and run `clawrouter login brk_live_…`. Calls
|
|
979
979
|
then go to `api.blockrun.ai` on a bearer token and bill your account credit — no
|
|
980
|
-
crypto, no gas, no seed phrase. And the <!-- br:models.free -->
|
|
980
|
+
crypto, no gas, no seed phrase. And the <!-- br:models.free -->7<!-- /br:models.free --> free models need neither: no
|
|
981
981
|
wallet, no key, no signup. See [Paying with a credit card](#paying-with-a-credit-card-api-key).
|
|
982
982
|
|
|
983
983
|
### Is ClawRouter free?
|
package/dist/cli.js
CHANGED
|
@@ -32315,6 +32315,11 @@ function getSharedSpendControl() {
|
|
|
32315
32315
|
host.__clawrouterSharedSpendControl ??= new SpendControl();
|
|
32316
32316
|
return host.__clawrouterSharedSpendControl;
|
|
32317
32317
|
}
|
|
32318
|
+
function parseQuotedAmountMicros(selected) {
|
|
32319
|
+
const raw = selected.amount ?? selected.maxAmountRequired;
|
|
32320
|
+
if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) return void 0;
|
|
32321
|
+
return BigInt(raw);
|
|
32322
|
+
}
|
|
32318
32323
|
function parseQuotedAmountUsd(selected) {
|
|
32319
32324
|
const raw = selected.amount ?? selected.maxAmountRequired;
|
|
32320
32325
|
if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) {
|
|
@@ -33080,6 +33085,16 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
|
|
|
33080
33085
|
return est === void 0 ? void 0 : Number(est);
|
|
33081
33086
|
};
|
|
33082
33087
|
const needMicros = estimateMicros();
|
|
33088
|
+
const notifyDeclinedPayment = () => {
|
|
33089
|
+
if (!options?.onPaymentDeclined) return;
|
|
33090
|
+
try {
|
|
33091
|
+
options.onPaymentDeclined();
|
|
33092
|
+
} catch (error) {
|
|
33093
|
+
console.error(
|
|
33094
|
+
`[ClawRouter] onPaymentDeclined callback failed: ${error instanceof Error ? error.message : String(error)}`
|
|
33095
|
+
);
|
|
33096
|
+
}
|
|
33097
|
+
};
|
|
33083
33098
|
const cached = !options?.skipPreAuth ? cache2.get(cacheKey2) : void 0;
|
|
33084
33099
|
const preAuthCovers = cached !== void 0 && Date.now() - cached.cachedAt < ttlMs && cached.coverMicros !== void 0 && needMicros !== void 0 && needMicros <= cached.coverMicros;
|
|
33085
33100
|
if (preAuthCovers) {
|
|
@@ -33098,6 +33113,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
|
|
|
33098
33113
|
return response2;
|
|
33099
33114
|
}
|
|
33100
33115
|
paymentInFlight = false;
|
|
33116
|
+
notifyDeclinedPayment();
|
|
33101
33117
|
cache2.delete(cacheKey2);
|
|
33102
33118
|
} catch (err) {
|
|
33103
33119
|
cache2.delete(cacheKey2);
|
|
@@ -33142,6 +33158,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
|
|
|
33142
33158
|
clonedRequest.headers.set(key3, value);
|
|
33143
33159
|
}
|
|
33144
33160
|
const paidResponse = await baseFetch(clonedRequest);
|
|
33161
|
+
if (paidResponse.status === 402) notifyDeclinedPayment();
|
|
33145
33162
|
notifyAcceptedPayment(paidResponse, payload);
|
|
33146
33163
|
return paidResponse;
|
|
33147
33164
|
};
|
|
@@ -64789,6 +64806,132 @@ var init_api_key = __esm({
|
|
|
64789
64806
|
}
|
|
64790
64807
|
});
|
|
64791
64808
|
|
|
64809
|
+
// src/session-spend.ts
|
|
64810
|
+
function parseMicros(raw) {
|
|
64811
|
+
if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
|
|
64812
|
+
return BigInt(raw);
|
|
64813
|
+
}
|
|
64814
|
+
function usdToMicros(usd2) {
|
|
64815
|
+
return BigInt(Math.round(usd2 * 1e6));
|
|
64816
|
+
}
|
|
64817
|
+
function microsToUsd(micros) {
|
|
64818
|
+
return Number(micros) / 1e6;
|
|
64819
|
+
}
|
|
64820
|
+
var SessionSpend, CostCapError;
|
|
64821
|
+
var init_session_spend = __esm({
|
|
64822
|
+
"src/session-spend.ts"() {
|
|
64823
|
+
"use strict";
|
|
64824
|
+
init_spend_control();
|
|
64825
|
+
SessionSpend = class _SessionSpend {
|
|
64826
|
+
constructor(store, sessionId, capMicros, holdMicros) {
|
|
64827
|
+
this.store = store;
|
|
64828
|
+
this.sessionId = sessionId;
|
|
64829
|
+
this.capMicros = capMicros;
|
|
64830
|
+
this.heldMicros = holdMicros;
|
|
64831
|
+
if (holdMicros > 0n) store.adjustSessionHold(sessionId, holdMicros);
|
|
64832
|
+
}
|
|
64833
|
+
store;
|
|
64834
|
+
sessionId;
|
|
64835
|
+
capMicros;
|
|
64836
|
+
/** Budget this request still holds against the session (strict mode only). */
|
|
64837
|
+
heldMicros;
|
|
64838
|
+
/** Sum of payments signed by this request that still count. */
|
|
64839
|
+
signedMicros = 0n;
|
|
64840
|
+
/** The most recent signature not yet settled or voided — the one a decline undoes. */
|
|
64841
|
+
lastSignedMicros = 0n;
|
|
64842
|
+
released = false;
|
|
64843
|
+
/**
|
|
64844
|
+
* Strict mode: admit the request only if the session's spend, plus what its
|
|
64845
|
+
* other in-flight requests hold, plus this request's estimate fits the cap —
|
|
64846
|
+
* and hold the estimate in the same step.
|
|
64847
|
+
*/
|
|
64848
|
+
static admit(store, sessionId, capMicros, estimateMicros) {
|
|
64849
|
+
const spentMicros = store.getSessionCostMicros(sessionId);
|
|
64850
|
+
const heldMicros = store.getSessionHoldMicros(sessionId);
|
|
64851
|
+
if (spentMicros + heldMicros + estimateMicros > capMicros) {
|
|
64852
|
+
return { admitted: false, spentMicros, heldMicros, estimateMicros };
|
|
64853
|
+
}
|
|
64854
|
+
return {
|
|
64855
|
+
admitted: true,
|
|
64856
|
+
spend: new _SessionSpend(store, sessionId, capMicros, estimateMicros)
|
|
64857
|
+
};
|
|
64858
|
+
}
|
|
64859
|
+
/** Graceful mode: track signed spend, enforce nothing. */
|
|
64860
|
+
static track(store, sessionId) {
|
|
64861
|
+
return new _SessionSpend(store, sessionId, void 0, 0n);
|
|
64862
|
+
}
|
|
64863
|
+
get isStrict() {
|
|
64864
|
+
return this.capMicros !== void 0;
|
|
64865
|
+
}
|
|
64866
|
+
/**
|
|
64867
|
+
* Before a payment is created: may this request sign `quoteMicros`?
|
|
64868
|
+
* Spend already recorded (including this request's own earlier signatures)
|
|
64869
|
+
* plus what OTHER in-flight requests hold, plus the quote, must fit the cap.
|
|
64870
|
+
* Always true in graceful mode.
|
|
64871
|
+
*/
|
|
64872
|
+
coverPayment(quoteMicros) {
|
|
64873
|
+
if (this.capMicros === void 0) return true;
|
|
64874
|
+
const spent = this.store.getSessionCostMicros(this.sessionId);
|
|
64875
|
+
const heldByOthers = this.store.getSessionHoldMicros(this.sessionId) - this.heldMicros;
|
|
64876
|
+
if (spent + heldByOthers + quoteMicros > this.capMicros) return false;
|
|
64877
|
+
if (quoteMicros > this.heldMicros) {
|
|
64878
|
+
this.store.adjustSessionHold(this.sessionId, quoteMicros - this.heldMicros);
|
|
64879
|
+
this.heldMicros = quoteMicros;
|
|
64880
|
+
}
|
|
64881
|
+
return true;
|
|
64882
|
+
}
|
|
64883
|
+
/** A payment was signed: it is spend now, and replaces part of the hold. */
|
|
64884
|
+
recordSigned(amountMicros) {
|
|
64885
|
+
if (amountMicros <= 0n) return;
|
|
64886
|
+
this.signedMicros += amountMicros;
|
|
64887
|
+
this.lastSignedMicros = amountMicros;
|
|
64888
|
+
this.store.addSessionCost(this.sessionId, amountMicros);
|
|
64889
|
+
this.shrinkHold(amountMicros);
|
|
64890
|
+
}
|
|
64891
|
+
/**
|
|
64892
|
+
* The gateway answered the payment just signed with a 402: nothing settled.
|
|
64893
|
+
* Payments within one request are signed and sent one at a time, so the
|
|
64894
|
+
* decline always belongs to the latest signature. Undone at most once.
|
|
64895
|
+
*/
|
|
64896
|
+
voidLastSigned() {
|
|
64897
|
+
const undo = this.lastSignedMicros;
|
|
64898
|
+
this.lastSignedMicros = 0n;
|
|
64899
|
+
if (undo <= 0n) return;
|
|
64900
|
+
this.signedMicros -= undo;
|
|
64901
|
+
this.store.addSessionCost(this.sessionId, -undo);
|
|
64902
|
+
}
|
|
64903
|
+
/**
|
|
64904
|
+
* An attempt succeeded. When nothing was signed for this request (the API
|
|
64905
|
+
* key rail), the local estimate is the only record of the spend. When a
|
|
64906
|
+
* payment was signed, it is already recorded at its real amount.
|
|
64907
|
+
*/
|
|
64908
|
+
recordSuccess(estimateMicros) {
|
|
64909
|
+
if (this.signedMicros > 0n || estimateMicros <= 0n) return;
|
|
64910
|
+
this.store.addSessionCost(this.sessionId, estimateMicros);
|
|
64911
|
+
this.shrinkHold(estimateMicros);
|
|
64912
|
+
}
|
|
64913
|
+
/** The request is over: return whatever it still holds. Idempotent. */
|
|
64914
|
+
release() {
|
|
64915
|
+
if (this.released) return;
|
|
64916
|
+
this.released = true;
|
|
64917
|
+
this.shrinkHold(this.heldMicros);
|
|
64918
|
+
}
|
|
64919
|
+
shrinkHold(byMicros) {
|
|
64920
|
+
const drop = byMicros < this.heldMicros ? byMicros : this.heldMicros;
|
|
64921
|
+
if (drop <= 0n) return;
|
|
64922
|
+
this.heldMicros -= drop;
|
|
64923
|
+
this.store.adjustSessionHold(this.sessionId, -drop);
|
|
64924
|
+
}
|
|
64925
|
+
};
|
|
64926
|
+
CostCapError = class extends SpendPolicyError {
|
|
64927
|
+
constructor(reason) {
|
|
64928
|
+
super(reason);
|
|
64929
|
+
this.name = "CostCapError";
|
|
64930
|
+
}
|
|
64931
|
+
};
|
|
64932
|
+
}
|
|
64933
|
+
});
|
|
64934
|
+
|
|
64792
64935
|
// src/compression/types.ts
|
|
64793
64936
|
var DEFAULT_COMPRESSION_CONFIG;
|
|
64794
64937
|
var init_types2 = __esm({
|
|
@@ -65636,6 +65779,9 @@ var init_session = __esm({
|
|
|
65636
65779
|
};
|
|
65637
65780
|
SessionStore = class {
|
|
65638
65781
|
sessions = /* @__PURE__ */ new Map();
|
|
65782
|
+
// Kept apart from `sessions`: a hold belongs to requests still running, so it
|
|
65783
|
+
// must not vanish when the idle-session sweep drops an entry mid-request.
|
|
65784
|
+
sessionHolds = /* @__PURE__ */ new Map();
|
|
65639
65785
|
config;
|
|
65640
65786
|
cleanupInterval = null;
|
|
65641
65787
|
constructor(config = {}) {
|
|
@@ -65811,14 +65957,34 @@ var init_session = __esm({
|
|
|
65811
65957
|
this.sessions.set(sessionId, entry);
|
|
65812
65958
|
}
|
|
65813
65959
|
entry.sessionCostMicros += additionalMicros;
|
|
65960
|
+
if (entry.sessionCostMicros < 0n) entry.sessionCostMicros = 0n;
|
|
65814
65961
|
}
|
|
65815
65962
|
/**
|
|
65816
65963
|
* Get the total accumulated cost for a session in USD.
|
|
65817
65964
|
*/
|
|
65818
65965
|
getSessionCostUsd(sessionId) {
|
|
65819
|
-
|
|
65820
|
-
|
|
65821
|
-
|
|
65966
|
+
return Number(this.getSessionCostMicros(sessionId)) / 1e6;
|
|
65967
|
+
}
|
|
65968
|
+
/**
|
|
65969
|
+
* Get the total accumulated cost for a session in USDC micros.
|
|
65970
|
+
*/
|
|
65971
|
+
getSessionCostMicros(sessionId) {
|
|
65972
|
+
return this.sessions.get(sessionId)?.sessionCostMicros ?? 0n;
|
|
65973
|
+
}
|
|
65974
|
+
/**
|
|
65975
|
+
* Budget held by a session's in-flight requests (USDC micros) — admitted
|
|
65976
|
+
* against the strict cap but not yet spent. See session-spend.ts.
|
|
65977
|
+
*/
|
|
65978
|
+
getSessionHoldMicros(sessionId) {
|
|
65979
|
+
return this.sessionHolds.get(sessionId) ?? 0n;
|
|
65980
|
+
}
|
|
65981
|
+
/**
|
|
65982
|
+
* Grow (positive) or release (negative) a session's in-flight hold.
|
|
65983
|
+
*/
|
|
65984
|
+
adjustSessionHold(sessionId, deltaMicros) {
|
|
65985
|
+
const next = (this.sessionHolds.get(sessionId) ?? 0n) + deltaMicros;
|
|
65986
|
+
if (next > 0n) this.sessionHolds.set(sessionId, next);
|
|
65987
|
+
else this.sessionHolds.delete(sessionId);
|
|
65822
65988
|
}
|
|
65823
65989
|
/**
|
|
65824
65990
|
* Stop the cleanup interval.
|
|
@@ -96521,6 +96687,19 @@ async function startProxy(options) {
|
|
|
96521
96687
|
}
|
|
96522
96688
|
const account = walletKey ? privateKeyToAccount(walletKey) : void 0;
|
|
96523
96689
|
const x402 = authMode === "wallet" ? new x402Client() : void 0;
|
|
96690
|
+
x402?.onBeforePaymentCreation(async (context) => {
|
|
96691
|
+
const spend = paymentStore.getStore()?.sessionSpend;
|
|
96692
|
+
if (!spend?.isStrict) return;
|
|
96693
|
+
const quote = parseQuotedAmountMicros(context.selectedRequirements);
|
|
96694
|
+
if (quote === void 0) {
|
|
96695
|
+
throw new CostCapError("ClawRouter cost cap: unreadable payment amount in the 402 quote");
|
|
96696
|
+
}
|
|
96697
|
+
if (!spend.coverPayment(quote)) {
|
|
96698
|
+
throw new CostCapError(
|
|
96699
|
+
`ClawRouter cost cap exceeded: signing $${microsToUsd(quote).toFixed(6)} would push this session past its maxCostPerRun limit`
|
|
96700
|
+
);
|
|
96701
|
+
}
|
|
96702
|
+
});
|
|
96524
96703
|
if (x402 && account) {
|
|
96525
96704
|
const evmPublicClient = createPublicClient({ chain: base, transport: http() });
|
|
96526
96705
|
const evmSigner = toClientEvmSigner(account, evmPublicClient);
|
|
@@ -96563,6 +96742,11 @@ async function startProxy(options) {
|
|
|
96563
96742
|
if (store) store.amountUsd = amountUsd;
|
|
96564
96743
|
console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
|
|
96565
96744
|
});
|
|
96745
|
+
x402?.onAfterPaymentCreation(async (context) => {
|
|
96746
|
+
const spend = paymentStore.getStore()?.sessionSpend;
|
|
96747
|
+
const amount = parseQuotedAmountMicros(context.selectedRequirements);
|
|
96748
|
+
if (spend && amount !== void 0) spend.recordSigned(amount);
|
|
96749
|
+
});
|
|
96566
96750
|
const payFetch = authMode === "api-key" ? withSpendPolicy(
|
|
96567
96751
|
createApiKeyFetch(apiKey, fetch, apiBase),
|
|
96568
96752
|
options.spendControl ?? getSharedSpendControl(),
|
|
@@ -96581,7 +96765,10 @@ async function startProxy(options) {
|
|
|
96581
96765
|
// Only the wallet rail settles per call. The API-key rail is billed
|
|
96582
96766
|
// server-side against account credit, so there is no per-request
|
|
96583
96767
|
// settlement for an observer to report.
|
|
96584
|
-
onPayment: options.onPayment
|
|
96768
|
+
onPayment: options.onPayment,
|
|
96769
|
+
// A 402 answer to a signed payment means the gateway declined it and
|
|
96770
|
+
// nothing settled — take it back off the session's budget.
|
|
96771
|
+
onPaymentDeclined: () => paymentStore.getStore()?.sessionSpend?.voidLastSigned()
|
|
96585
96772
|
});
|
|
96586
96773
|
let balanceMonitor;
|
|
96587
96774
|
if (options._balanceMonitorOverride) {
|
|
@@ -97865,6 +98052,17 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
97865
98052
|
return { success: true, response };
|
|
97866
98053
|
} catch (err) {
|
|
97867
98054
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
98055
|
+
if (err instanceof CostCapError) {
|
|
98056
|
+
return {
|
|
98057
|
+
success: false,
|
|
98058
|
+
errorBody: JSON.stringify({
|
|
98059
|
+
error: { message: errorMsg, type: "cost_cap_exceeded", code: "cost_cap_exceeded" }
|
|
98060
|
+
}),
|
|
98061
|
+
errorStatus: 429,
|
|
98062
|
+
isProviderError: false,
|
|
98063
|
+
errorCategory: "payment_error"
|
|
98064
|
+
};
|
|
98065
|
+
}
|
|
97868
98066
|
if (err instanceof SpendPolicyError) {
|
|
97869
98067
|
return {
|
|
97870
98068
|
success: false,
|
|
@@ -98944,14 +99142,23 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
98944
99142
|
}
|
|
98945
99143
|
}
|
|
98946
99144
|
}
|
|
99145
|
+
let sessionSpend;
|
|
98947
99146
|
if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "strict") {
|
|
98948
|
-
const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
|
|
98949
99147
|
const thisReqEstStr = estimatedCostMicros !== void 0 ? estimatedCostMicros.toString() : modelId ? estimateAmount(modelId, body.length, maxTokens) : void 0;
|
|
98950
|
-
const
|
|
98951
|
-
|
|
98952
|
-
|
|
99148
|
+
const admission = SessionSpend.admit(
|
|
99149
|
+
sessionStore,
|
|
99150
|
+
effectiveSessionId,
|
|
99151
|
+
usdToMicros(options.maxCostPerRunUsd),
|
|
99152
|
+
parseMicros(thisReqEstStr) ?? 0n
|
|
99153
|
+
);
|
|
99154
|
+
if (!admission.admitted) {
|
|
99155
|
+
const spentUsd = microsToUsd(admission.spentMicros);
|
|
99156
|
+
const heldUsd = microsToUsd(admission.heldMicros);
|
|
99157
|
+
const estUsd = microsToUsd(admission.estimateMicros);
|
|
99158
|
+
const projectedCostUsd = spentUsd + heldUsd + estUsd;
|
|
99159
|
+
const breakdown = `spent $${spentUsd.toFixed(4)}${heldUsd > 0 ? ` + in flight $${heldUsd.toFixed(4)}` : ""} + est $${estUsd.toFixed(4)}`;
|
|
98953
99160
|
console.log(
|
|
98954
|
-
`[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (
|
|
99161
|
+
`[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (${breakdown}) > $${options.maxCostPerRunUsd} limit`
|
|
98955
99162
|
);
|
|
98956
99163
|
res.writeHead(429, {
|
|
98957
99164
|
"Content-Type": "application/json",
|
|
@@ -98960,7 +99167,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
98960
99167
|
res.end(
|
|
98961
99168
|
JSON.stringify({
|
|
98962
99169
|
error: {
|
|
98963
|
-
message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (
|
|
99170
|
+
message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (${breakdown}) would exceed limit $${options.maxCostPerRunUsd}`,
|
|
98964
99171
|
type: "cost_cap_exceeded",
|
|
98965
99172
|
code: "cost_cap_exceeded"
|
|
98966
99173
|
}
|
|
@@ -98969,7 +99176,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
98969
99176
|
deduplicator.removeInflight(dedupKey);
|
|
98970
99177
|
return;
|
|
98971
99178
|
}
|
|
99179
|
+
sessionSpend = admission.spend;
|
|
99180
|
+
} else if (options.maxCostPerRunUsd && effectiveSessionId) {
|
|
99181
|
+
sessionSpend = SessionSpend.track(sessionStore, effectiveSessionId);
|
|
98972
99182
|
}
|
|
99183
|
+
const requestStore = paymentStore.getStore();
|
|
99184
|
+
if (requestStore) requestStore.sessionSpend = sessionSpend;
|
|
98973
99185
|
if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
|
|
98974
99186
|
const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
|
|
98975
99187
|
const remainingUsd = options.maxCostPerRunUsd - runCostUsd;
|
|
@@ -99365,11 +99577,9 @@ data: [DONE]
|
|
|
99365
99577
|
actualModelUsed = tryModel;
|
|
99366
99578
|
recordOutcome(tryModel, requestKind, true);
|
|
99367
99579
|
console.log(`[ClawRouter] Success with model: ${tryModel}`);
|
|
99368
|
-
if (
|
|
99369
|
-
const costEst = estimateAmount(tryModel, body.length, maxTokens);
|
|
99370
|
-
if (costEst)
|
|
99371
|
-
sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
|
|
99372
|
-
}
|
|
99580
|
+
if (sessionSpend && !FREE_MODELS.has(tryModel)) {
|
|
99581
|
+
const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
|
|
99582
|
+
if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
|
|
99373
99583
|
}
|
|
99374
99584
|
break;
|
|
99375
99585
|
}
|
|
@@ -99443,11 +99653,9 @@ data: [DONE]
|
|
|
99443
99653
|
upstream = retryResult.response;
|
|
99444
99654
|
actualModelUsed = tryModel;
|
|
99445
99655
|
console.log(`[ClawRouter] Explicit-pin retry succeeded for: ${tryModel}`);
|
|
99446
|
-
if (
|
|
99447
|
-
const costEst = estimateAmount(tryModel, body.length, maxTokens);
|
|
99448
|
-
if (costEst)
|
|
99449
|
-
sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
|
|
99450
|
-
}
|
|
99656
|
+
if (sessionSpend && !FREE_MODELS.has(tryModel)) {
|
|
99657
|
+
const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
|
|
99658
|
+
if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
|
|
99451
99659
|
}
|
|
99452
99660
|
break;
|
|
99453
99661
|
}
|
|
@@ -99514,11 +99722,9 @@ data: [DONE]
|
|
|
99514
99722
|
upstream = retryResult.response;
|
|
99515
99723
|
actualModelUsed = tryModel;
|
|
99516
99724
|
console.log(`[ClawRouter] Rate-limit retry succeeded for: ${tryModel}`);
|
|
99517
|
-
if (
|
|
99518
|
-
const costEst = estimateAmount(tryModel, body.length, maxTokens);
|
|
99519
|
-
if (costEst)
|
|
99520
|
-
sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
|
|
99521
|
-
}
|
|
99725
|
+
if (sessionSpend && !FREE_MODELS.has(tryModel)) {
|
|
99726
|
+
const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
|
|
99727
|
+
if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
|
|
99522
99728
|
}
|
|
99523
99729
|
break;
|
|
99524
99730
|
}
|
|
@@ -99612,6 +99818,7 @@ data: [DONE]
|
|
|
99612
99818
|
);
|
|
99613
99819
|
const rawErrBody = lastError?.body || structuredMessage;
|
|
99614
99820
|
const errStatus = lastError?.status || 502;
|
|
99821
|
+
const costCapRefusal = errStatus === 429 && rawErrBody.includes('"cost_cap_exceeded"');
|
|
99615
99822
|
const transformedErr = transformPaymentError(rawErrBody);
|
|
99616
99823
|
requestHadError = true;
|
|
99617
99824
|
if (headersSentEarly) {
|
|
@@ -99648,7 +99855,8 @@ data: [DONE]
|
|
|
99648
99855
|
"Content-Type": "application/json",
|
|
99649
99856
|
"x-context-used-kb": String(originalContextSizeKB),
|
|
99650
99857
|
"x-context-limit-kb": String(CONTEXT_LIMIT_KB),
|
|
99651
|
-
...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {}
|
|
99858
|
+
...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {},
|
|
99859
|
+
...costCapRefusal ? { "X-ClawRouter-Cost-Cap-Exceeded": "1" } : {}
|
|
99652
99860
|
});
|
|
99653
99861
|
res.end(transformedErr);
|
|
99654
99862
|
deduplicator.complete(dedupKey, {
|
|
@@ -100082,6 +100290,8 @@ data: [DONE]
|
|
|
100082
100290
|
throw new Error(`Request timed out after ${timeoutMs}ms`, { cause: err });
|
|
100083
100291
|
}
|
|
100084
100292
|
throw err;
|
|
100293
|
+
} finally {
|
|
100294
|
+
sessionSpend?.release();
|
|
100085
100295
|
}
|
|
100086
100296
|
const logModel = routingDecision?.model ?? modelId;
|
|
100087
100297
|
if (logModel) {
|
|
@@ -100168,6 +100378,7 @@ var init_proxy = __esm({
|
|
|
100168
100378
|
init_auth();
|
|
100169
100379
|
init_api_key();
|
|
100170
100380
|
init_spend_control();
|
|
100381
|
+
init_session_spend();
|
|
100171
100382
|
init_compression();
|
|
100172
100383
|
init_errors5();
|
|
100173
100384
|
init_version4();
|
|
@@ -123261,7 +123472,7 @@ var init_client5 = __esm({
|
|
|
123261
123472
|
function resolveSpendControl(deps) {
|
|
123262
123473
|
return deps?.spendControl ?? getSharedSpendControl();
|
|
123263
123474
|
}
|
|
123264
|
-
function
|
|
123475
|
+
function usdToMicros2(usd2) {
|
|
123265
123476
|
return String(Math.ceil(usd2 * 1e6));
|
|
123266
123477
|
}
|
|
123267
123478
|
async function signUnderSpendPolicy(deps, quote, action, sign4) {
|
|
@@ -123527,7 +123738,7 @@ Re-call with confirm:true to sign and submit.`,
|
|
|
123527
123738
|
payTo: negRisk ? NEG_RISK_CTF_EXCHANGE_V2 : CTF_EXCHANGE_V2,
|
|
123528
123739
|
network: `eip155:${POLYGON_CHAIN_ID}`,
|
|
123529
123740
|
asset: PUSD_COLLATERAL,
|
|
123530
|
-
amount:
|
|
123741
|
+
amount: usdToMicros2(notional)
|
|
123531
123742
|
};
|
|
123532
123743
|
let r2;
|
|
123533
123744
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -966,6 +966,7 @@ declare const DEFAULT_SESSION_CONFIG: SessionConfig;
|
|
|
966
966
|
*/
|
|
967
967
|
declare class SessionStore {
|
|
968
968
|
private sessions;
|
|
969
|
+
private sessionHolds;
|
|
969
970
|
private config;
|
|
970
971
|
private cleanupInterval;
|
|
971
972
|
constructor(config?: Partial<SessionConfig>);
|
|
@@ -1036,6 +1037,19 @@ declare class SessionStore {
|
|
|
1036
1037
|
* Get the total accumulated cost for a session in USD.
|
|
1037
1038
|
*/
|
|
1038
1039
|
getSessionCostUsd(sessionId: string): number;
|
|
1040
|
+
/**
|
|
1041
|
+
* Get the total accumulated cost for a session in USDC micros.
|
|
1042
|
+
*/
|
|
1043
|
+
getSessionCostMicros(sessionId: string): bigint;
|
|
1044
|
+
/**
|
|
1045
|
+
* Budget held by a session's in-flight requests (USDC micros) — admitted
|
|
1046
|
+
* against the strict cap but not yet spent. See session-spend.ts.
|
|
1047
|
+
*/
|
|
1048
|
+
getSessionHoldMicros(sessionId: string): bigint;
|
|
1049
|
+
/**
|
|
1050
|
+
* Grow (positive) or release (negative) a session's in-flight hold.
|
|
1051
|
+
*/
|
|
1052
|
+
adjustSessionHold(sessionId: string, deltaMicros: bigint): void;
|
|
1039
1053
|
/**
|
|
1040
1054
|
* Stop the cleanup interval.
|
|
1041
1055
|
*/
|
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.283",
|
|
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
|
|