@blockrun/llm 3.14.3 → 3.15.0
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 +42 -7
- package/dist/index.cjs +242 -168
- package/dist/index.d.cts +101 -1
- package/dist/index.d.ts +101 -1
- package/dist/index.js +242 -168
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ console.log(r.response); // the proof
|
|
|
65
65
|
## Installation
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
npm install @blockrun/llm #
|
|
68
|
+
npm install @blockrun/llm # Account API keys or Base wallets; smart routing included
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
<details>
|
|
@@ -98,6 +98,8 @@ without them throws an error naming the exact install command.
|
|
|
98
98
|
|
|
99
99
|
</details>
|
|
100
100
|
|
|
101
|
+
The Anthropic SDK is a runtime dependency because the public compatibility wrapper exposes its types. Solana signing dependencies remain optional and are unnecessary for account billing.
|
|
102
|
+
|
|
101
103
|
## Quick Start: API Key
|
|
102
104
|
|
|
103
105
|
1. [Sign up or sign in](https://user.blockrun.ai).
|
|
@@ -130,9 +132,14 @@ wallet mode even when `BLOCKRUN_API_KEY` is set; passing both explicit credentia
|
|
|
130
132
|
is an error. With no explicit credential, `BLOCKRUN_API_KEY` beats the wallet key
|
|
131
133
|
env vars — a process holding both runs in account mode. Invalid or exhausted API keys never fall back to wallet payments.
|
|
132
134
|
Errors preserve `statusCode`, account error `response.code`, and `retryAfter`.
|
|
133
|
-
Account credentials are restricted to the configured origin, including polling.
|
|
135
|
+
Account credentials are restricted to the configured origin, including polling. Account POSTs are not automatically replayed, including through the Anthropic wrapper. GET/HEAD requests can retry temporary gateway errors; accepted jobs keep polling the same job within the original timeout. A 429 returns `retryAfter` to the caller.
|
|
136
|
+
|
|
137
|
+
Check [Activity](https://user.blockrun.ai/dashboard/activity) for account usage and charges. Chat uses token usage; media and data can use duration, image, or per-request units. When adding credit, the checkout shows the credit amount and total card charge, including any processing fee. Keep API keys in server or local environment variables, never in browser code or logs.
|
|
138
|
+
|
|
139
|
+
For asynchronous jobs, retain the complete returned `poll_url`, including its query parameters. If polling times out, check the original job and Activity before submitting again.
|
|
140
|
+
|
|
141
|
+
To switch back to a wallet, unset `BLOCKRUN_API_KEY` and create a new wallet client, or pass an explicit `privateKey` to the appropriate client. Existing clients retain their original credentials; changing a wallet chain does not change account billing.
|
|
134
142
|
|
|
135
|
-
Use the [account dashboard](https://user.blockrun.ai/dashboard) for account usage.
|
|
136
143
|
`getSpending()` reports x402 settlements only and throws in account mode; wallet
|
|
137
144
|
address/balance helpers require a wallet. Account credit does not sign trades or
|
|
138
145
|
transfer wallet funds. Service availability depends on the account gateway and model.
|
|
@@ -1017,8 +1024,7 @@ Generic escape hatches: `client.defi(path, params)`, `client.dex(path, params, b
|
|
|
1017
1024
|
`RpcClient` wraps `POST /v1/rpc/{network}` — standard JSON-RPC 2.0 access to
|
|
1018
1025
|
<!-- br:chains.rpc -->40<!-- /br:chains.rpc --> chains through one endpoint (Ethereum, Base, Solana, Polygon, BSC,
|
|
1019
1026
|
Arbitrum, Optimism, Avalanche, Bitcoin, Sui, and more; powered by Tatum's RPC
|
|
1020
|
-
gateway).
|
|
1021
|
-
USDC; a JSON-RPC batch charges per element.
|
|
1027
|
+
gateway). Use account credits or the selected x402 wallet; no separate Tatum key is needed. A JSON-RPC batch is priced per element.
|
|
1022
1028
|
|
|
1023
1029
|
```ts
|
|
1024
1030
|
import { RpcClient } from '@blockrun/llm';
|
|
@@ -1233,6 +1239,35 @@ while (true) {
|
|
|
1233
1239
|
}
|
|
1234
1240
|
```
|
|
1235
1241
|
|
|
1242
|
+
#### Solana
|
|
1243
|
+
|
|
1244
|
+
`SolanaLLMClient.stream()` is the Solana counterpart, with the same shape as
|
|
1245
|
+
`BlockrunClient.stream()`: it yields each `data:` frame already parsed as JSON
|
|
1246
|
+
and stops at `[DONE]`. The x402 handshake happens before the first frame — 402,
|
|
1247
|
+
sign an SPL TransferChecked authorization locally, replay with
|
|
1248
|
+
`PAYMENT-SIGNATURE` — including the fresh-blockhash re-sign on a
|
|
1249
|
+
verification-phase rejection. A free model, or an account key, is answered `200`
|
|
1250
|
+
with no handshake at all and settles nothing.
|
|
1251
|
+
|
|
1252
|
+
```typescript
|
|
1253
|
+
import { SolanaLLMClient } from '@blockrun/llm';
|
|
1254
|
+
|
|
1255
|
+
const client = new SolanaLLMClient(); // SOLANA_WALLET_KEY, or { apiKey: 'brk_live_…' }
|
|
1256
|
+
|
|
1257
|
+
for await (const chunk of client.stream('/v1/chat/completions', {
|
|
1258
|
+
model: 'deepseek/deepseek-chat',
|
|
1259
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
1260
|
+
max_tokens: 64,
|
|
1261
|
+
stream: true,
|
|
1262
|
+
})) {
|
|
1263
|
+
process.stdout.write(chunk?.choices?.[0]?.delta?.content ?? '');
|
|
1264
|
+
}
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
Set `stream: true` yourself — the method does not inject it, because the gateway
|
|
1268
|
+
prices a streaming and a non-streaming request the same and rewriting a caller's
|
|
1269
|
+
body silently is how you end up debugging a request you did not send.
|
|
1270
|
+
|
|
1236
1271
|
#### Payment + streaming flow
|
|
1237
1272
|
|
|
1238
1273
|
```
|
|
@@ -1278,7 +1313,7 @@ const [gpt, claude, gemini] = await Promise.all([
|
|
|
1278
1313
|
|
|
1279
1314
|
## Prediction Markets (Powered by Predexon)
|
|
1280
1315
|
|
|
1281
|
-
Access real-time prediction market data from Polymarket, Kalshi, and Binance Futures via [Predexon](https://predexon.com).
|
|
1316
|
+
Access real-time prediction market data from Polymarket, Kalshi, and Binance Futures via [Predexon](https://predexon.com). Use a BlockRun account API key or x402 wallet payments; no separate Predexon key is needed.
|
|
1282
1317
|
|
|
1283
1318
|
### Polymarket
|
|
1284
1319
|
|
|
@@ -1339,7 +1374,7 @@ Works on both `LLMClient` (Base) and `SolanaLLMClient`.
|
|
|
1339
1374
|
|
|
1340
1375
|
## Exa Web Search (Powered by Exa)
|
|
1341
1376
|
|
|
1342
|
-
Access [Exa](https://exa.ai)'s neural web search
|
|
1377
|
+
Access [Exa](https://exa.ai)'s neural web search using account credits or x402 wallet payments; no separate Exa key is needed. Use `SolanaLLMClient` for a Solana wallet or `LLMClient` for a Base wallet. Availability depends on the selected gateway.
|
|
1343
1378
|
|
|
1344
1379
|
| Method | Description |
|
|
1345
1380
|
|---|---|
|
package/dist/index.cjs
CHANGED
|
@@ -275,6 +275,21 @@ function validateResourceUrl(url, baseUrl) {
|
|
|
275
275
|
var TRANSIENT_STATUS = /* @__PURE__ */ new Set([502, 503, 504, 522, 524]);
|
|
276
276
|
var TRANSIENT_RETRIES = 2;
|
|
277
277
|
var TRANSIENT_BACKOFF_MS = 1e3;
|
|
278
|
+
function retryDelay(ms, signal) {
|
|
279
|
+
signal?.throwIfAborted();
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
281
|
+
const onAbort = () => {
|
|
282
|
+
clearTimeout(timer);
|
|
283
|
+
signal?.removeEventListener("abort", onAbort);
|
|
284
|
+
reject(signal?.reason);
|
|
285
|
+
};
|
|
286
|
+
const timer = setTimeout(() => {
|
|
287
|
+
signal?.removeEventListener("abort", onAbort);
|
|
288
|
+
resolve();
|
|
289
|
+
}, ms);
|
|
290
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
291
|
+
});
|
|
292
|
+
}
|
|
278
293
|
var API_KEY_URL = "https://api.blockrun.ai";
|
|
279
294
|
var PORTAL_URL = "https://user.blockrun.ai";
|
|
280
295
|
function resolveApiKeyAuth(options) {
|
|
@@ -324,6 +339,7 @@ var ApiKeyAuth = class {
|
|
|
324
339
|
headers.set("authorization", `Bearer ${this.#key}`);
|
|
325
340
|
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
326
341
|
const retries = method === "GET" || method === "HEAD" ? TRANSIENT_RETRIES : 0;
|
|
342
|
+
const signal = init?.signal ?? request?.signal;
|
|
327
343
|
let response;
|
|
328
344
|
for (let attempt = 0; ; attempt++) {
|
|
329
345
|
response = await globalThis.fetch(request ? new Request(url, request) : url, {
|
|
@@ -332,7 +348,8 @@ var ApiKeyAuth = class {
|
|
|
332
348
|
redirect: "error"
|
|
333
349
|
});
|
|
334
350
|
if (attempt >= retries || !TRANSIENT_STATUS.has(response.status)) break;
|
|
335
|
-
await
|
|
351
|
+
await response.body?.cancel();
|
|
352
|
+
await retryDelay(TRANSIENT_BACKOFF_MS * (attempt + 1), signal);
|
|
336
353
|
}
|
|
337
354
|
if (raiseErrors && !response.ok) {
|
|
338
355
|
let body;
|
|
@@ -4600,7 +4617,7 @@ function getCostSummary() {
|
|
|
4600
4617
|
}
|
|
4601
4618
|
|
|
4602
4619
|
// src/version.ts
|
|
4603
|
-
var SDK_VERSION = "3.
|
|
4620
|
+
var SDK_VERSION = "3.15.0";
|
|
4604
4621
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
4605
4622
|
|
|
4606
4623
|
// src/client.ts
|
|
@@ -6861,7 +6878,7 @@ var VideoClient = class {
|
|
|
6861
6878
|
* Submits an async job, then polls until the video is ready. Typical total
|
|
6862
6879
|
* wall-time is 60-180s, but upstream status can lag several minutes behind
|
|
6863
6880
|
* actual completion. If upstream runs past the budget (default 15min),
|
|
6864
|
-
* throws
|
|
6881
|
+
* throws with the existing poll URL; check billing before submitting again.
|
|
6865
6882
|
*
|
|
6866
6883
|
* @param prompt - Text description of the video
|
|
6867
6884
|
* @param options - Optional generation parameters
|
|
@@ -7061,7 +7078,7 @@ var VideoClient = class {
|
|
|
7061
7078
|
}
|
|
7062
7079
|
}
|
|
7063
7080
|
throw new APIError(
|
|
7064
|
-
`Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}).
|
|
7081
|
+
`Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). A polling timeout does not confirm billing status. Resume the existing poll_url using the same account or wallet; check account Activity or wallet receipts before submitting another job.`,
|
|
7065
7082
|
504,
|
|
7066
7083
|
{ id: submitData.id, last_status: lastStatus, poll_url: pollUrl }
|
|
7067
7084
|
);
|
|
@@ -8553,6 +8570,35 @@ var RpcClient = class {
|
|
|
8553
8570
|
}
|
|
8554
8571
|
};
|
|
8555
8572
|
|
|
8573
|
+
// src/sse.ts
|
|
8574
|
+
async function* readSseFrames(response, onMissingBody) {
|
|
8575
|
+
if (!response.body) throw onMissingBody(response.status);
|
|
8576
|
+
const reader = response.body.getReader();
|
|
8577
|
+
const decoder = new TextDecoder();
|
|
8578
|
+
let buffer = "";
|
|
8579
|
+
try {
|
|
8580
|
+
while (true) {
|
|
8581
|
+
const { done, value } = await reader.read();
|
|
8582
|
+
if (done) break;
|
|
8583
|
+
buffer += decoder.decode(value, { stream: true });
|
|
8584
|
+
const lines = buffer.split("\n");
|
|
8585
|
+
buffer = lines.pop() || "";
|
|
8586
|
+
for (const line of lines) {
|
|
8587
|
+
const trimmed = line.trim();
|
|
8588
|
+
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
8589
|
+
const data = trimmed.slice(6);
|
|
8590
|
+
if (data === "[DONE]") return;
|
|
8591
|
+
try {
|
|
8592
|
+
yield JSON.parse(data);
|
|
8593
|
+
} catch {
|
|
8594
|
+
}
|
|
8595
|
+
}
|
|
8596
|
+
}
|
|
8597
|
+
} finally {
|
|
8598
|
+
reader.releaseLock();
|
|
8599
|
+
}
|
|
8600
|
+
}
|
|
8601
|
+
|
|
8556
8602
|
// src/blockrun.ts
|
|
8557
8603
|
var import_accounts14 = require("viem/accounts");
|
|
8558
8604
|
var DEFAULT_API_URL13 = "https://blockrun.ai/api";
|
|
@@ -8762,33 +8808,10 @@ var BlockrunClient = class {
|
|
|
8762
8808
|
await this.throwApiError(resp402, `stream failed (${url})`);
|
|
8763
8809
|
return;
|
|
8764
8810
|
}
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
const decoder = new TextDecoder();
|
|
8770
|
-
let buffer = "";
|
|
8771
|
-
try {
|
|
8772
|
-
while (true) {
|
|
8773
|
-
const { done, value } = await reader.read();
|
|
8774
|
-
if (done) break;
|
|
8775
|
-
buffer += decoder.decode(value, { stream: true });
|
|
8776
|
-
const lines = buffer.split("\n");
|
|
8777
|
-
buffer = lines.pop() || "";
|
|
8778
|
-
for (const line of lines) {
|
|
8779
|
-
const trimmed = line.trim();
|
|
8780
|
-
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
8781
|
-
const data = trimmed.slice(6);
|
|
8782
|
-
if (data === "[DONE]") return;
|
|
8783
|
-
try {
|
|
8784
|
-
yield JSON.parse(data);
|
|
8785
|
-
} catch {
|
|
8786
|
-
}
|
|
8787
|
-
}
|
|
8788
|
-
}
|
|
8789
|
-
} finally {
|
|
8790
|
-
reader.releaseLock();
|
|
8791
|
-
}
|
|
8811
|
+
yield* readSseFrames(
|
|
8812
|
+
streamResp,
|
|
8813
|
+
(status2) => new APIError("Stream response has no body", status2, {})
|
|
8814
|
+
);
|
|
8792
8815
|
}
|
|
8793
8816
|
// --------------------------------------------------------------------
|
|
8794
8817
|
// Internal: shared infrastructure
|
|
@@ -9719,6 +9742,123 @@ var SolanaLLMClient = class {
|
|
|
9719
9742
|
response.routing = decision;
|
|
9720
9743
|
return { response, model: decision.model, routing: decision };
|
|
9721
9744
|
}
|
|
9745
|
+
/**
|
|
9746
|
+
* Stream a Server-Sent Events endpoint, paid on Solana.
|
|
9747
|
+
*
|
|
9748
|
+
* The Solana counterpart to `BlockrunClient.stream`, and the reason it had to
|
|
9749
|
+
* exist: a streaming harness cannot use this client at all without it, so
|
|
9750
|
+
* "BlockRun supports Solana" stopped being true the moment a caller streamed.
|
|
9751
|
+
* `chatCompletion` buffers the whole answer, which is the wrong shape for an
|
|
9752
|
+
* agent loop and for anything that shows tokens as they arrive.
|
|
9753
|
+
*
|
|
9754
|
+
* The handshake is the one the non-streaming paths use — `402`, sign an SPL
|
|
9755
|
+
* TransferChecked authorization locally, replay with `PAYMENT-SIGNATURE` —
|
|
9756
|
+
* with the same verification-phase re-sign on a stale blockhash. What differs
|
|
9757
|
+
* is that the paid response is not read as JSON: it is handed to the SSE
|
|
9758
|
+
* reader with its body untouched.
|
|
9759
|
+
*
|
|
9760
|
+
* A `200` on the first request is returned as-is and settles nothing. That is
|
|
9761
|
+
* the free tier (the gateway answers a `billing_mode: "free"` model without a
|
|
9762
|
+
* 402 at all) and it is also API-key mode, where billing is on the account
|
|
9763
|
+
* rather than on a wallet.
|
|
9764
|
+
*
|
|
9765
|
+
* Yields each `data:` frame parsed as JSON, and stops at `data: [DONE]`.
|
|
9766
|
+
* Malformed frames are skipped rather than thrown — see {@link readSseFrames}.
|
|
9767
|
+
*
|
|
9768
|
+
* @example
|
|
9769
|
+
* for await (const chunk of client.stream<ChatChunk>("/v1/chat/completions", {
|
|
9770
|
+
* model: "deepseek/deepseek-chat",
|
|
9771
|
+
* messages: [{ role: "user", content: "Hi" }],
|
|
9772
|
+
* stream: true,
|
|
9773
|
+
* })) {
|
|
9774
|
+
* process.stdout.write(chunk.choices?.[0]?.delta?.content ?? "");
|
|
9775
|
+
* }
|
|
9776
|
+
*
|
|
9777
|
+
* @param path - endpoint after the API root; a leading `/api` is tolerated.
|
|
9778
|
+
* @param body - JSON request body. Set `stream: true` yourself — this method
|
|
9779
|
+
* does not inject it, because the gateway prices a streaming and a
|
|
9780
|
+
* non-streaming request the same and silently rewriting a caller's body is
|
|
9781
|
+
* how you end up debugging a request you did not send.
|
|
9782
|
+
* @returns each decoded SSE frame, in order.
|
|
9783
|
+
*/
|
|
9784
|
+
async *stream(path6, body) {
|
|
9785
|
+
const url = this.buildUrl(path6);
|
|
9786
|
+
const response = await this.openPaidStream(url, JSON.stringify(body ?? {}));
|
|
9787
|
+
yield* readSseFrames(
|
|
9788
|
+
response,
|
|
9789
|
+
(status2) => new APIError("Stream response has no body", status2, {})
|
|
9790
|
+
);
|
|
9791
|
+
}
|
|
9792
|
+
/**
|
|
9793
|
+
* Get to a streaming response, paying for it if the gateway asks.
|
|
9794
|
+
*
|
|
9795
|
+
* Separate from {@link SolanaLLMClient.stream} because a generator cannot
|
|
9796
|
+
* retry cleanly around a `yield`: the stale-blockhash re-sign has to finish
|
|
9797
|
+
* before the first frame is handed out, and putting the loop here keeps the
|
|
9798
|
+
* payment decision entirely ahead of any output the caller has seen.
|
|
9799
|
+
*
|
|
9800
|
+
* @param url - resolved endpoint URL.
|
|
9801
|
+
* @param requestBody - the serialized body, reused verbatim on the paid retry
|
|
9802
|
+
* so the gateway prices and answers the same request it quoted for.
|
|
9803
|
+
* @returns a response whose body has not been read.
|
|
9804
|
+
*/
|
|
9805
|
+
async openPaidStream(url, requestBody) {
|
|
9806
|
+
for (let staleRetries = 0; ; ) {
|
|
9807
|
+
const response = await this.fetchWithTimeout(url, {
|
|
9808
|
+
method: "POST",
|
|
9809
|
+
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
9810
|
+
body: requestBody
|
|
9811
|
+
});
|
|
9812
|
+
if (response.ok) return response;
|
|
9813
|
+
if (response.status !== 402) {
|
|
9814
|
+
let errorBody;
|
|
9815
|
+
try {
|
|
9816
|
+
errorBody = await response.json();
|
|
9817
|
+
} catch {
|
|
9818
|
+
errorBody = { error: "Request failed" };
|
|
9819
|
+
}
|
|
9820
|
+
throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
|
|
9821
|
+
}
|
|
9822
|
+
try {
|
|
9823
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(
|
|
9824
|
+
url,
|
|
9825
|
+
response,
|
|
9826
|
+
staleRetries > 0
|
|
9827
|
+
);
|
|
9828
|
+
const paid = await this.fetchWithTimeout(url, {
|
|
9829
|
+
method: "POST",
|
|
9830
|
+
headers: {
|
|
9831
|
+
"Content-Type": "application/json",
|
|
9832
|
+
"User-Agent": USER_AGENT,
|
|
9833
|
+
"PAYMENT-SIGNATURE": paymentPayload
|
|
9834
|
+
},
|
|
9835
|
+
body: requestBody
|
|
9836
|
+
});
|
|
9837
|
+
await this.assertPaid(paid);
|
|
9838
|
+
this.recordSettlement(costUsd);
|
|
9839
|
+
return paid;
|
|
9840
|
+
} catch (error) {
|
|
9841
|
+
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
9842
|
+
await waitForStaleRetry(staleRetries++);
|
|
9843
|
+
continue;
|
|
9844
|
+
}
|
|
9845
|
+
}
|
|
9846
|
+
}
|
|
9847
|
+
/**
|
|
9848
|
+
* Resolve an endpoint path against this client's API root.
|
|
9849
|
+
*
|
|
9850
|
+
* A leading `/api` is stripped for the same reason `BlockrunClient` strips
|
|
9851
|
+
* it: the documented paths are written `/api/v1/…` on the website and
|
|
9852
|
+
* `/v1/…` in this SDK, and a caller who copies one into the other should get
|
|
9853
|
+
* their request rather than a 404.
|
|
9854
|
+
* @param path - endpoint path, with or without a leading slash.
|
|
9855
|
+
* @returns the absolute URL to call.
|
|
9856
|
+
*/
|
|
9857
|
+
buildUrl(path6) {
|
|
9858
|
+
let normalized = path6.startsWith("/") ? path6 : `/${path6}`;
|
|
9859
|
+
if (normalized.startsWith("/api/")) normalized = normalized.slice(4);
|
|
9860
|
+
return `${this.apiUrl}${normalized}`;
|
|
9861
|
+
}
|
|
9722
9862
|
/** List available models. */
|
|
9723
9863
|
async listModels() {
|
|
9724
9864
|
const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, { method: "GET" });
|
|
@@ -9989,7 +10129,25 @@ var SolanaLLMClient = class {
|
|
|
9989
10129
|
return response.json();
|
|
9990
10130
|
}
|
|
9991
10131
|
}
|
|
9992
|
-
|
|
10132
|
+
/**
|
|
10133
|
+
* Turn a `402` into a signed Solana payment payload.
|
|
10134
|
+
*
|
|
10135
|
+
* Extracted because four call sites need it — chat, the raw POST helpers, the
|
|
10136
|
+
* raw GET helper, and {@link SolanaLLMClient.stream} — and it had been
|
|
10137
|
+
* written out three times before this. That mattered more than ordinary
|
|
10138
|
+
* duplication: this is the code that signs a transfer of the caller's USDC,
|
|
10139
|
+
* so three copies meant every fix to it had to be applied three times or
|
|
10140
|
+
* quietly apply to two thirds of the paths.
|
|
10141
|
+
*
|
|
10142
|
+
* @param url - the request being paid for.
|
|
10143
|
+
* @param response - the gateway's `402`, not yet consumed.
|
|
10144
|
+
* @param forceFreshBlockhash - set on a re-sign after a stale-blockhash
|
|
10145
|
+
* rejection, so the retry cannot produce byte-identical transaction bytes.
|
|
10146
|
+
* @param resourceFallback - resource URL to claim when the 402 states none.
|
|
10147
|
+
* @returns the header value to replay with, and what it will settle for.
|
|
10148
|
+
* @throws PaymentError when the 402 carries no usable Solana requirements.
|
|
10149
|
+
*/
|
|
10150
|
+
async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
|
|
9993
10151
|
let paymentHeader = response.headers.get("payment-required");
|
|
9994
10152
|
if (!paymentHeader) {
|
|
9995
10153
|
try {
|
|
@@ -10023,7 +10181,7 @@ var SolanaLLMClient = class {
|
|
|
10023
10181
|
feePayer,
|
|
10024
10182
|
{
|
|
10025
10183
|
resourceUrl: validateResourceUrl(
|
|
10026
|
-
details.resource?.url ||
|
|
10184
|
+
details.resource?.url || resourceFallback,
|
|
10027
10185
|
this.apiUrl
|
|
10028
10186
|
),
|
|
10029
10187
|
resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
|
|
@@ -10035,6 +10193,15 @@ var SolanaLLMClient = class {
|
|
|
10035
10193
|
forceFreshBlockhash
|
|
10036
10194
|
}
|
|
10037
10195
|
);
|
|
10196
|
+
return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
|
|
10197
|
+
}
|
|
10198
|
+
async handlePaymentAndRetry(url, body, response, forceFreshBlockhash = false) {
|
|
10199
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(
|
|
10200
|
+
url,
|
|
10201
|
+
response,
|
|
10202
|
+
forceFreshBlockhash,
|
|
10203
|
+
`${this.apiUrl}/v1/chat/completions`
|
|
10204
|
+
);
|
|
10038
10205
|
const retryResponse = await this.fetchWithTimeout(url, {
|
|
10039
10206
|
method: "POST",
|
|
10040
10207
|
headers: {
|
|
@@ -10044,24 +10211,8 @@ var SolanaLLMClient = class {
|
|
|
10044
10211
|
},
|
|
10045
10212
|
body: JSON.stringify(body)
|
|
10046
10213
|
});
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
throw new SafeStaleBlockhashError();
|
|
10050
|
-
}
|
|
10051
|
-
throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
|
|
10052
|
-
}
|
|
10053
|
-
if (!retryResponse.ok) {
|
|
10054
|
-
let errorBody;
|
|
10055
|
-
try {
|
|
10056
|
-
errorBody = await retryResponse.json();
|
|
10057
|
-
} catch {
|
|
10058
|
-
errorBody = { error: "Request failed" };
|
|
10059
|
-
}
|
|
10060
|
-
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
10061
|
-
}
|
|
10062
|
-
const costUsd = parseFloat(details.amount) / 1e6;
|
|
10063
|
-
this.sessionCalls += 1;
|
|
10064
|
-
this.sessionTotalUsd += costUsd;
|
|
10214
|
+
await this.assertPaid(retryResponse);
|
|
10215
|
+
this.recordSettlement(costUsd);
|
|
10065
10216
|
return retryResponse.json();
|
|
10066
10217
|
}
|
|
10067
10218
|
async requestWithPaymentRaw(endpoint, body) {
|
|
@@ -10095,51 +10246,7 @@ var SolanaLLMClient = class {
|
|
|
10095
10246
|
}
|
|
10096
10247
|
}
|
|
10097
10248
|
async handlePaymentAndRetryRaw(url, body, response, forceFreshBlockhash = false) {
|
|
10098
|
-
|
|
10099
|
-
if (!paymentHeader) {
|
|
10100
|
-
try {
|
|
10101
|
-
const respBody = await response.json();
|
|
10102
|
-
if (respBody.accepts || respBody.x402Version) {
|
|
10103
|
-
paymentHeader = btoa(JSON.stringify(respBody));
|
|
10104
|
-
}
|
|
10105
|
-
} catch {
|
|
10106
|
-
}
|
|
10107
|
-
}
|
|
10108
|
-
if (!paymentHeader) {
|
|
10109
|
-
throw new PaymentError("402 response but no payment requirements found");
|
|
10110
|
-
}
|
|
10111
|
-
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
10112
|
-
const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
|
|
10113
|
-
if (!details.network?.startsWith("solana:")) {
|
|
10114
|
-
throw new PaymentError(
|
|
10115
|
-
`Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
|
|
10116
|
-
);
|
|
10117
|
-
}
|
|
10118
|
-
const feePayer = details.extra?.feePayer;
|
|
10119
|
-
if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
|
|
10120
|
-
const fromAddress = await this.getWalletAddress();
|
|
10121
|
-
const secretKey = await solanaKeyToBytes(this.privateKey);
|
|
10122
|
-
const extensions = paymentRequired.extensions;
|
|
10123
|
-
const paymentPayload = await createSolanaPaymentPayload(
|
|
10124
|
-
secretKey,
|
|
10125
|
-
fromAddress,
|
|
10126
|
-
details.recipient,
|
|
10127
|
-
details.amount,
|
|
10128
|
-
feePayer,
|
|
10129
|
-
{
|
|
10130
|
-
resourceUrl: validateResourceUrl(
|
|
10131
|
-
details.resource?.url || url,
|
|
10132
|
-
this.apiUrl
|
|
10133
|
-
),
|
|
10134
|
-
resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
|
|
10135
|
-
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
|
|
10136
|
-
extra: details.extra,
|
|
10137
|
-
extensions,
|
|
10138
|
-
rpcUrl: this.rpcUrl,
|
|
10139
|
-
rpcHeaders: this.rpcHeaders,
|
|
10140
|
-
forceFreshBlockhash
|
|
10141
|
-
}
|
|
10142
|
-
);
|
|
10249
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(url, response, forceFreshBlockhash);
|
|
10143
10250
|
const retryResponse = await this.fetchWithTimeout(url, {
|
|
10144
10251
|
method: "POST",
|
|
10145
10252
|
headers: {
|
|
@@ -10149,24 +10256,8 @@ var SolanaLLMClient = class {
|
|
|
10149
10256
|
},
|
|
10150
10257
|
body: JSON.stringify(body)
|
|
10151
10258
|
});
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
throw new SafeStaleBlockhashError();
|
|
10155
|
-
}
|
|
10156
|
-
throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
|
|
10157
|
-
}
|
|
10158
|
-
if (!retryResponse.ok) {
|
|
10159
|
-
let errorBody;
|
|
10160
|
-
try {
|
|
10161
|
-
errorBody = await retryResponse.json();
|
|
10162
|
-
} catch {
|
|
10163
|
-
errorBody = { error: "Request failed" };
|
|
10164
|
-
}
|
|
10165
|
-
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
10166
|
-
}
|
|
10167
|
-
const costUsd = parseFloat(details.amount) / 1e6;
|
|
10168
|
-
this.sessionCalls += 1;
|
|
10169
|
-
this.sessionTotalUsd += costUsd;
|
|
10259
|
+
await this.assertPaid(retryResponse);
|
|
10260
|
+
this.recordSettlement(costUsd);
|
|
10170
10261
|
return retryResponse.json();
|
|
10171
10262
|
}
|
|
10172
10263
|
async getWithPaymentRaw(endpoint, params) {
|
|
@@ -10199,51 +10290,7 @@ var SolanaLLMClient = class {
|
|
|
10199
10290
|
}
|
|
10200
10291
|
}
|
|
10201
10292
|
async handleGetPaymentAndRetryRaw(url, endpoint, params, response, forceFreshBlockhash = false) {
|
|
10202
|
-
|
|
10203
|
-
if (!paymentHeader) {
|
|
10204
|
-
try {
|
|
10205
|
-
const respBody = await response.json();
|
|
10206
|
-
if (respBody.accepts || respBody.x402Version) {
|
|
10207
|
-
paymentHeader = btoa(JSON.stringify(respBody));
|
|
10208
|
-
}
|
|
10209
|
-
} catch {
|
|
10210
|
-
}
|
|
10211
|
-
}
|
|
10212
|
-
if (!paymentHeader) {
|
|
10213
|
-
throw new PaymentError("402 response but no payment requirements found");
|
|
10214
|
-
}
|
|
10215
|
-
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
10216
|
-
const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
|
|
10217
|
-
if (!details.network?.startsWith("solana:")) {
|
|
10218
|
-
throw new PaymentError(
|
|
10219
|
-
`Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
|
|
10220
|
-
);
|
|
10221
|
-
}
|
|
10222
|
-
const feePayer = details.extra?.feePayer;
|
|
10223
|
-
if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
|
|
10224
|
-
const fromAddress = await this.getWalletAddress();
|
|
10225
|
-
const secretKey = await solanaKeyToBytes(this.privateKey);
|
|
10226
|
-
const extensions = paymentRequired.extensions;
|
|
10227
|
-
const paymentPayload = await createSolanaPaymentPayload(
|
|
10228
|
-
secretKey,
|
|
10229
|
-
fromAddress,
|
|
10230
|
-
details.recipient,
|
|
10231
|
-
details.amount,
|
|
10232
|
-
feePayer,
|
|
10233
|
-
{
|
|
10234
|
-
resourceUrl: validateResourceUrl(
|
|
10235
|
-
details.resource?.url || url,
|
|
10236
|
-
this.apiUrl
|
|
10237
|
-
),
|
|
10238
|
-
resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
|
|
10239
|
-
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
|
|
10240
|
-
extra: details.extra,
|
|
10241
|
-
extensions,
|
|
10242
|
-
rpcUrl: this.rpcUrl,
|
|
10243
|
-
rpcHeaders: this.rpcHeaders,
|
|
10244
|
-
forceFreshBlockhash
|
|
10245
|
-
}
|
|
10246
|
-
);
|
|
10293
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(url, response, forceFreshBlockhash);
|
|
10247
10294
|
const query = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
10248
10295
|
const retryUrl = `${this.apiUrl}${endpoint}${query}`;
|
|
10249
10296
|
const retryResponse = await this.fetchWithTimeout(retryUrl, {
|
|
@@ -10253,25 +10300,44 @@ var SolanaLLMClient = class {
|
|
|
10253
10300
|
"PAYMENT-SIGNATURE": paymentPayload
|
|
10254
10301
|
}
|
|
10255
10302
|
});
|
|
10256
|
-
|
|
10257
|
-
|
|
10303
|
+
await this.assertPaid(retryResponse);
|
|
10304
|
+
this.recordSettlement(costUsd);
|
|
10305
|
+
return retryResponse.json();
|
|
10306
|
+
}
|
|
10307
|
+
/**
|
|
10308
|
+
* Fail a post-payment response, telling a re-signable rejection from a real one.
|
|
10309
|
+
*
|
|
10310
|
+
* A `402` here is not "pay again": it is the gateway refusing the payment we
|
|
10311
|
+
* just signed. Only a rejection the gateway attributes to the VERIFICATION
|
|
10312
|
+
* phase is safe to re-sign — anything settled, or ambiguous about which
|
|
10313
|
+
* phase it failed in, could already have moved USDC, and re-signing it would
|
|
10314
|
+
* pay twice. {@link isSafeStaleBlockhashResponse} is where that judgement
|
|
10315
|
+
* lives.
|
|
10316
|
+
* @param response - the reply to the paid request.
|
|
10317
|
+
* @throws SafeStaleBlockhashError when the caller should re-sign, PaymentError
|
|
10318
|
+
* when it should not, APIError for any other failure.
|
|
10319
|
+
*/
|
|
10320
|
+
async assertPaid(response) {
|
|
10321
|
+
if (response.status === 402) {
|
|
10322
|
+
if (await isSafeStaleBlockhashResponse(response)) {
|
|
10258
10323
|
throw new SafeStaleBlockhashError();
|
|
10259
10324
|
}
|
|
10260
10325
|
throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
|
|
10261
10326
|
}
|
|
10262
|
-
if (!
|
|
10327
|
+
if (!response.ok) {
|
|
10263
10328
|
let errorBody;
|
|
10264
10329
|
try {
|
|
10265
|
-
errorBody = await
|
|
10330
|
+
errorBody = await response.json();
|
|
10266
10331
|
} catch {
|
|
10267
10332
|
errorBody = { error: "Request failed" };
|
|
10268
10333
|
}
|
|
10269
|
-
throw new APIError(`API error after payment: ${
|
|
10334
|
+
throw new APIError(`API error after payment: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
|
|
10270
10335
|
}
|
|
10271
|
-
|
|
10336
|
+
}
|
|
10337
|
+
/** Count one settled x402 payment against the session total. */
|
|
10338
|
+
recordSettlement(costUsd) {
|
|
10272
10339
|
this.sessionCalls += 1;
|
|
10273
10340
|
this.sessionTotalUsd += costUsd;
|
|
10274
|
-
return retryResponse.json();
|
|
10275
10341
|
}
|
|
10276
10342
|
async fetchWithTimeout(url, options) {
|
|
10277
10343
|
const controller = new AbortController();
|
|
@@ -10675,7 +10741,8 @@ var OpenAI = class {
|
|
|
10675
10741
|
}
|
|
10676
10742
|
client;
|
|
10677
10743
|
constructor(options = {}) {
|
|
10678
|
-
const
|
|
10744
|
+
const aliases = [options.walletKey, options.privateKey].filter((k) => k !== void 0);
|
|
10745
|
+
const privateKey = aliases.find((k) => k !== "") ?? aliases[0];
|
|
10679
10746
|
const apiUrl = resolveApiKeyAuth({ apiKey: options.apiKey, privateKey, apiUrl: options.baseURL })?.apiUrl ?? options.baseURL ?? "https://blockrun.ai/api";
|
|
10680
10747
|
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
10681
10748
|
this.client = new LLMClient({
|
|
@@ -10734,7 +10801,9 @@ var AnthropicClient = class {
|
|
|
10734
10801
|
this._client = new Anthropic({
|
|
10735
10802
|
baseURL: this._apiUrl,
|
|
10736
10803
|
apiKey: "blockrun",
|
|
10737
|
-
fetch: this._x402Fetch.bind(this)
|
|
10804
|
+
fetch: this._x402Fetch.bind(this),
|
|
10805
|
+
// Account POSTs may already be billed when an upstream error arrives.
|
|
10806
|
+
...this.apiAuth ? { maxRetries: 0 } : {}
|
|
10738
10807
|
});
|
|
10739
10808
|
return this._client;
|
|
10740
10809
|
})();
|
|
@@ -10743,6 +10812,10 @@ var AnthropicClient = class {
|
|
|
10743
10812
|
async _x402Fetch(input, init) {
|
|
10744
10813
|
const controller = new AbortController();
|
|
10745
10814
|
const timeoutId = setTimeout(() => controller.abort(), this._timeout);
|
|
10815
|
+
const callerSignal = init?.signal ?? (input instanceof Request ? input.signal : void 0);
|
|
10816
|
+
const onAbort = () => controller.abort(callerSignal?.reason);
|
|
10817
|
+
callerSignal?.addEventListener("abort", onAbort, { once: true });
|
|
10818
|
+
if (callerSignal?.aborted) onAbort();
|
|
10746
10819
|
try {
|
|
10747
10820
|
const mergedInit = { ...init, signal: controller.signal };
|
|
10748
10821
|
if (this.apiAuth) return await this.apiAuth.fetch(input, mergedInit, false);
|
|
@@ -10796,6 +10869,7 @@ var AnthropicClient = class {
|
|
|
10796
10869
|
}
|
|
10797
10870
|
return response;
|
|
10798
10871
|
} finally {
|
|
10872
|
+
callerSignal?.removeEventListener("abort", onAbort);
|
|
10799
10873
|
clearTimeout(timeoutId);
|
|
10800
10874
|
}
|
|
10801
10875
|
}
|