@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/dist/index.js
CHANGED
|
@@ -158,6 +158,21 @@ function validateResourceUrl(url, baseUrl) {
|
|
|
158
158
|
var TRANSIENT_STATUS = /* @__PURE__ */ new Set([502, 503, 504, 522, 524]);
|
|
159
159
|
var TRANSIENT_RETRIES = 2;
|
|
160
160
|
var TRANSIENT_BACKOFF_MS = 1e3;
|
|
161
|
+
function retryDelay(ms, signal) {
|
|
162
|
+
signal?.throwIfAborted();
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const onAbort = () => {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
signal?.removeEventListener("abort", onAbort);
|
|
167
|
+
reject(signal?.reason);
|
|
168
|
+
};
|
|
169
|
+
const timer = setTimeout(() => {
|
|
170
|
+
signal?.removeEventListener("abort", onAbort);
|
|
171
|
+
resolve();
|
|
172
|
+
}, ms);
|
|
173
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
174
|
+
});
|
|
175
|
+
}
|
|
161
176
|
var API_KEY_URL = "https://api.blockrun.ai";
|
|
162
177
|
var PORTAL_URL = "https://user.blockrun.ai";
|
|
163
178
|
function resolveApiKeyAuth(options) {
|
|
@@ -207,6 +222,7 @@ var ApiKeyAuth = class {
|
|
|
207
222
|
headers.set("authorization", `Bearer ${this.#key}`);
|
|
208
223
|
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
209
224
|
const retries = method === "GET" || method === "HEAD" ? TRANSIENT_RETRIES : 0;
|
|
225
|
+
const signal = init?.signal ?? request?.signal;
|
|
210
226
|
let response;
|
|
211
227
|
for (let attempt = 0; ; attempt++) {
|
|
212
228
|
response = await globalThis.fetch(request ? new Request(url, request) : url, {
|
|
@@ -215,7 +231,8 @@ var ApiKeyAuth = class {
|
|
|
215
231
|
redirect: "error"
|
|
216
232
|
});
|
|
217
233
|
if (attempt >= retries || !TRANSIENT_STATUS.has(response.status)) break;
|
|
218
|
-
await
|
|
234
|
+
await response.body?.cancel();
|
|
235
|
+
await retryDelay(TRANSIENT_BACKOFF_MS * (attempt + 1), signal);
|
|
219
236
|
}
|
|
220
237
|
if (raiseErrors && !response.ok) {
|
|
221
238
|
let body;
|
|
@@ -4483,7 +4500,7 @@ function getCostSummary() {
|
|
|
4483
4500
|
}
|
|
4484
4501
|
|
|
4485
4502
|
// src/version.ts
|
|
4486
|
-
var SDK_VERSION = "3.
|
|
4503
|
+
var SDK_VERSION = "3.15.0";
|
|
4487
4504
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
4488
4505
|
|
|
4489
4506
|
// src/client.ts
|
|
@@ -6744,7 +6761,7 @@ var VideoClient = class {
|
|
|
6744
6761
|
* Submits an async job, then polls until the video is ready. Typical total
|
|
6745
6762
|
* wall-time is 60-180s, but upstream status can lag several minutes behind
|
|
6746
6763
|
* actual completion. If upstream runs past the budget (default 15min),
|
|
6747
|
-
* throws
|
|
6764
|
+
* throws with the existing poll URL; check billing before submitting again.
|
|
6748
6765
|
*
|
|
6749
6766
|
* @param prompt - Text description of the video
|
|
6750
6767
|
* @param options - Optional generation parameters
|
|
@@ -6944,7 +6961,7 @@ var VideoClient = class {
|
|
|
6944
6961
|
}
|
|
6945
6962
|
}
|
|
6946
6963
|
throw new APIError(
|
|
6947
|
-
`Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}).
|
|
6964
|
+
`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.`,
|
|
6948
6965
|
504,
|
|
6949
6966
|
{ id: submitData.id, last_status: lastStatus, poll_url: pollUrl }
|
|
6950
6967
|
);
|
|
@@ -8436,6 +8453,35 @@ var RpcClient = class {
|
|
|
8436
8453
|
}
|
|
8437
8454
|
};
|
|
8438
8455
|
|
|
8456
|
+
// src/sse.ts
|
|
8457
|
+
async function* readSseFrames(response, onMissingBody) {
|
|
8458
|
+
if (!response.body) throw onMissingBody(response.status);
|
|
8459
|
+
const reader = response.body.getReader();
|
|
8460
|
+
const decoder = new TextDecoder();
|
|
8461
|
+
let buffer = "";
|
|
8462
|
+
try {
|
|
8463
|
+
while (true) {
|
|
8464
|
+
const { done, value } = await reader.read();
|
|
8465
|
+
if (done) break;
|
|
8466
|
+
buffer += decoder.decode(value, { stream: true });
|
|
8467
|
+
const lines = buffer.split("\n");
|
|
8468
|
+
buffer = lines.pop() || "";
|
|
8469
|
+
for (const line of lines) {
|
|
8470
|
+
const trimmed = line.trim();
|
|
8471
|
+
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
8472
|
+
const data = trimmed.slice(6);
|
|
8473
|
+
if (data === "[DONE]") return;
|
|
8474
|
+
try {
|
|
8475
|
+
yield JSON.parse(data);
|
|
8476
|
+
} catch {
|
|
8477
|
+
}
|
|
8478
|
+
}
|
|
8479
|
+
}
|
|
8480
|
+
} finally {
|
|
8481
|
+
reader.releaseLock();
|
|
8482
|
+
}
|
|
8483
|
+
}
|
|
8484
|
+
|
|
8439
8485
|
// src/blockrun.ts
|
|
8440
8486
|
import { privateKeyToAccount as privateKeyToAccount13 } from "viem/accounts";
|
|
8441
8487
|
var DEFAULT_API_URL13 = "https://blockrun.ai/api";
|
|
@@ -8645,33 +8691,10 @@ var BlockrunClient = class {
|
|
|
8645
8691
|
await this.throwApiError(resp402, `stream failed (${url})`);
|
|
8646
8692
|
return;
|
|
8647
8693
|
}
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
const decoder = new TextDecoder();
|
|
8653
|
-
let buffer = "";
|
|
8654
|
-
try {
|
|
8655
|
-
while (true) {
|
|
8656
|
-
const { done, value } = await reader.read();
|
|
8657
|
-
if (done) break;
|
|
8658
|
-
buffer += decoder.decode(value, { stream: true });
|
|
8659
|
-
const lines = buffer.split("\n");
|
|
8660
|
-
buffer = lines.pop() || "";
|
|
8661
|
-
for (const line of lines) {
|
|
8662
|
-
const trimmed = line.trim();
|
|
8663
|
-
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
8664
|
-
const data = trimmed.slice(6);
|
|
8665
|
-
if (data === "[DONE]") return;
|
|
8666
|
-
try {
|
|
8667
|
-
yield JSON.parse(data);
|
|
8668
|
-
} catch {
|
|
8669
|
-
}
|
|
8670
|
-
}
|
|
8671
|
-
}
|
|
8672
|
-
} finally {
|
|
8673
|
-
reader.releaseLock();
|
|
8674
|
-
}
|
|
8694
|
+
yield* readSseFrames(
|
|
8695
|
+
streamResp,
|
|
8696
|
+
(status2) => new APIError("Stream response has no body", status2, {})
|
|
8697
|
+
);
|
|
8675
8698
|
}
|
|
8676
8699
|
// --------------------------------------------------------------------
|
|
8677
8700
|
// Internal: shared infrastructure
|
|
@@ -9602,6 +9625,123 @@ var SolanaLLMClient = class {
|
|
|
9602
9625
|
response.routing = decision;
|
|
9603
9626
|
return { response, model: decision.model, routing: decision };
|
|
9604
9627
|
}
|
|
9628
|
+
/**
|
|
9629
|
+
* Stream a Server-Sent Events endpoint, paid on Solana.
|
|
9630
|
+
*
|
|
9631
|
+
* The Solana counterpart to `BlockrunClient.stream`, and the reason it had to
|
|
9632
|
+
* exist: a streaming harness cannot use this client at all without it, so
|
|
9633
|
+
* "BlockRun supports Solana" stopped being true the moment a caller streamed.
|
|
9634
|
+
* `chatCompletion` buffers the whole answer, which is the wrong shape for an
|
|
9635
|
+
* agent loop and for anything that shows tokens as they arrive.
|
|
9636
|
+
*
|
|
9637
|
+
* The handshake is the one the non-streaming paths use — `402`, sign an SPL
|
|
9638
|
+
* TransferChecked authorization locally, replay with `PAYMENT-SIGNATURE` —
|
|
9639
|
+
* with the same verification-phase re-sign on a stale blockhash. What differs
|
|
9640
|
+
* is that the paid response is not read as JSON: it is handed to the SSE
|
|
9641
|
+
* reader with its body untouched.
|
|
9642
|
+
*
|
|
9643
|
+
* A `200` on the first request is returned as-is and settles nothing. That is
|
|
9644
|
+
* the free tier (the gateway answers a `billing_mode: "free"` model without a
|
|
9645
|
+
* 402 at all) and it is also API-key mode, where billing is on the account
|
|
9646
|
+
* rather than on a wallet.
|
|
9647
|
+
*
|
|
9648
|
+
* Yields each `data:` frame parsed as JSON, and stops at `data: [DONE]`.
|
|
9649
|
+
* Malformed frames are skipped rather than thrown — see {@link readSseFrames}.
|
|
9650
|
+
*
|
|
9651
|
+
* @example
|
|
9652
|
+
* for await (const chunk of client.stream<ChatChunk>("/v1/chat/completions", {
|
|
9653
|
+
* model: "deepseek/deepseek-chat",
|
|
9654
|
+
* messages: [{ role: "user", content: "Hi" }],
|
|
9655
|
+
* stream: true,
|
|
9656
|
+
* })) {
|
|
9657
|
+
* process.stdout.write(chunk.choices?.[0]?.delta?.content ?? "");
|
|
9658
|
+
* }
|
|
9659
|
+
*
|
|
9660
|
+
* @param path - endpoint after the API root; a leading `/api` is tolerated.
|
|
9661
|
+
* @param body - JSON request body. Set `stream: true` yourself — this method
|
|
9662
|
+
* does not inject it, because the gateway prices a streaming and a
|
|
9663
|
+
* non-streaming request the same and silently rewriting a caller's body is
|
|
9664
|
+
* how you end up debugging a request you did not send.
|
|
9665
|
+
* @returns each decoded SSE frame, in order.
|
|
9666
|
+
*/
|
|
9667
|
+
async *stream(path6, body) {
|
|
9668
|
+
const url = this.buildUrl(path6);
|
|
9669
|
+
const response = await this.openPaidStream(url, JSON.stringify(body ?? {}));
|
|
9670
|
+
yield* readSseFrames(
|
|
9671
|
+
response,
|
|
9672
|
+
(status2) => new APIError("Stream response has no body", status2, {})
|
|
9673
|
+
);
|
|
9674
|
+
}
|
|
9675
|
+
/**
|
|
9676
|
+
* Get to a streaming response, paying for it if the gateway asks.
|
|
9677
|
+
*
|
|
9678
|
+
* Separate from {@link SolanaLLMClient.stream} because a generator cannot
|
|
9679
|
+
* retry cleanly around a `yield`: the stale-blockhash re-sign has to finish
|
|
9680
|
+
* before the first frame is handed out, and putting the loop here keeps the
|
|
9681
|
+
* payment decision entirely ahead of any output the caller has seen.
|
|
9682
|
+
*
|
|
9683
|
+
* @param url - resolved endpoint URL.
|
|
9684
|
+
* @param requestBody - the serialized body, reused verbatim on the paid retry
|
|
9685
|
+
* so the gateway prices and answers the same request it quoted for.
|
|
9686
|
+
* @returns a response whose body has not been read.
|
|
9687
|
+
*/
|
|
9688
|
+
async openPaidStream(url, requestBody) {
|
|
9689
|
+
for (let staleRetries = 0; ; ) {
|
|
9690
|
+
const response = await this.fetchWithTimeout(url, {
|
|
9691
|
+
method: "POST",
|
|
9692
|
+
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
9693
|
+
body: requestBody
|
|
9694
|
+
});
|
|
9695
|
+
if (response.ok) return response;
|
|
9696
|
+
if (response.status !== 402) {
|
|
9697
|
+
let errorBody;
|
|
9698
|
+
try {
|
|
9699
|
+
errorBody = await response.json();
|
|
9700
|
+
} catch {
|
|
9701
|
+
errorBody = { error: "Request failed" };
|
|
9702
|
+
}
|
|
9703
|
+
throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
|
|
9704
|
+
}
|
|
9705
|
+
try {
|
|
9706
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(
|
|
9707
|
+
url,
|
|
9708
|
+
response,
|
|
9709
|
+
staleRetries > 0
|
|
9710
|
+
);
|
|
9711
|
+
const paid = await this.fetchWithTimeout(url, {
|
|
9712
|
+
method: "POST",
|
|
9713
|
+
headers: {
|
|
9714
|
+
"Content-Type": "application/json",
|
|
9715
|
+
"User-Agent": USER_AGENT,
|
|
9716
|
+
"PAYMENT-SIGNATURE": paymentPayload
|
|
9717
|
+
},
|
|
9718
|
+
body: requestBody
|
|
9719
|
+
});
|
|
9720
|
+
await this.assertPaid(paid);
|
|
9721
|
+
this.recordSettlement(costUsd);
|
|
9722
|
+
return paid;
|
|
9723
|
+
} catch (error) {
|
|
9724
|
+
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
9725
|
+
await waitForStaleRetry(staleRetries++);
|
|
9726
|
+
continue;
|
|
9727
|
+
}
|
|
9728
|
+
}
|
|
9729
|
+
}
|
|
9730
|
+
/**
|
|
9731
|
+
* Resolve an endpoint path against this client's API root.
|
|
9732
|
+
*
|
|
9733
|
+
* A leading `/api` is stripped for the same reason `BlockrunClient` strips
|
|
9734
|
+
* it: the documented paths are written `/api/v1/…` on the website and
|
|
9735
|
+
* `/v1/…` in this SDK, and a caller who copies one into the other should get
|
|
9736
|
+
* their request rather than a 404.
|
|
9737
|
+
* @param path - endpoint path, with or without a leading slash.
|
|
9738
|
+
* @returns the absolute URL to call.
|
|
9739
|
+
*/
|
|
9740
|
+
buildUrl(path6) {
|
|
9741
|
+
let normalized = path6.startsWith("/") ? path6 : `/${path6}`;
|
|
9742
|
+
if (normalized.startsWith("/api/")) normalized = normalized.slice(4);
|
|
9743
|
+
return `${this.apiUrl}${normalized}`;
|
|
9744
|
+
}
|
|
9605
9745
|
/** List available models. */
|
|
9606
9746
|
async listModels() {
|
|
9607
9747
|
const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, { method: "GET" });
|
|
@@ -9872,7 +10012,25 @@ var SolanaLLMClient = class {
|
|
|
9872
10012
|
return response.json();
|
|
9873
10013
|
}
|
|
9874
10014
|
}
|
|
9875
|
-
|
|
10015
|
+
/**
|
|
10016
|
+
* Turn a `402` into a signed Solana payment payload.
|
|
10017
|
+
*
|
|
10018
|
+
* Extracted because four call sites need it — chat, the raw POST helpers, the
|
|
10019
|
+
* raw GET helper, and {@link SolanaLLMClient.stream} — and it had been
|
|
10020
|
+
* written out three times before this. That mattered more than ordinary
|
|
10021
|
+
* duplication: this is the code that signs a transfer of the caller's USDC,
|
|
10022
|
+
* so three copies meant every fix to it had to be applied three times or
|
|
10023
|
+
* quietly apply to two thirds of the paths.
|
|
10024
|
+
*
|
|
10025
|
+
* @param url - the request being paid for.
|
|
10026
|
+
* @param response - the gateway's `402`, not yet consumed.
|
|
10027
|
+
* @param forceFreshBlockhash - set on a re-sign after a stale-blockhash
|
|
10028
|
+
* rejection, so the retry cannot produce byte-identical transaction bytes.
|
|
10029
|
+
* @param resourceFallback - resource URL to claim when the 402 states none.
|
|
10030
|
+
* @returns the header value to replay with, and what it will settle for.
|
|
10031
|
+
* @throws PaymentError when the 402 carries no usable Solana requirements.
|
|
10032
|
+
*/
|
|
10033
|
+
async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
|
|
9876
10034
|
let paymentHeader = response.headers.get("payment-required");
|
|
9877
10035
|
if (!paymentHeader) {
|
|
9878
10036
|
try {
|
|
@@ -9906,7 +10064,7 @@ var SolanaLLMClient = class {
|
|
|
9906
10064
|
feePayer,
|
|
9907
10065
|
{
|
|
9908
10066
|
resourceUrl: validateResourceUrl(
|
|
9909
|
-
details.resource?.url ||
|
|
10067
|
+
details.resource?.url || resourceFallback,
|
|
9910
10068
|
this.apiUrl
|
|
9911
10069
|
),
|
|
9912
10070
|
resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
|
|
@@ -9918,6 +10076,15 @@ var SolanaLLMClient = class {
|
|
|
9918
10076
|
forceFreshBlockhash
|
|
9919
10077
|
}
|
|
9920
10078
|
);
|
|
10079
|
+
return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
|
|
10080
|
+
}
|
|
10081
|
+
async handlePaymentAndRetry(url, body, response, forceFreshBlockhash = false) {
|
|
10082
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(
|
|
10083
|
+
url,
|
|
10084
|
+
response,
|
|
10085
|
+
forceFreshBlockhash,
|
|
10086
|
+
`${this.apiUrl}/v1/chat/completions`
|
|
10087
|
+
);
|
|
9921
10088
|
const retryResponse = await this.fetchWithTimeout(url, {
|
|
9922
10089
|
method: "POST",
|
|
9923
10090
|
headers: {
|
|
@@ -9927,24 +10094,8 @@ var SolanaLLMClient = class {
|
|
|
9927
10094
|
},
|
|
9928
10095
|
body: JSON.stringify(body)
|
|
9929
10096
|
});
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
throw new SafeStaleBlockhashError();
|
|
9933
|
-
}
|
|
9934
|
-
throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
|
|
9935
|
-
}
|
|
9936
|
-
if (!retryResponse.ok) {
|
|
9937
|
-
let errorBody;
|
|
9938
|
-
try {
|
|
9939
|
-
errorBody = await retryResponse.json();
|
|
9940
|
-
} catch {
|
|
9941
|
-
errorBody = { error: "Request failed" };
|
|
9942
|
-
}
|
|
9943
|
-
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
9944
|
-
}
|
|
9945
|
-
const costUsd = parseFloat(details.amount) / 1e6;
|
|
9946
|
-
this.sessionCalls += 1;
|
|
9947
|
-
this.sessionTotalUsd += costUsd;
|
|
10097
|
+
await this.assertPaid(retryResponse);
|
|
10098
|
+
this.recordSettlement(costUsd);
|
|
9948
10099
|
return retryResponse.json();
|
|
9949
10100
|
}
|
|
9950
10101
|
async requestWithPaymentRaw(endpoint, body) {
|
|
@@ -9978,51 +10129,7 @@ var SolanaLLMClient = class {
|
|
|
9978
10129
|
}
|
|
9979
10130
|
}
|
|
9980
10131
|
async handlePaymentAndRetryRaw(url, body, response, forceFreshBlockhash = false) {
|
|
9981
|
-
|
|
9982
|
-
if (!paymentHeader) {
|
|
9983
|
-
try {
|
|
9984
|
-
const respBody = await response.json();
|
|
9985
|
-
if (respBody.accepts || respBody.x402Version) {
|
|
9986
|
-
paymentHeader = btoa(JSON.stringify(respBody));
|
|
9987
|
-
}
|
|
9988
|
-
} catch {
|
|
9989
|
-
}
|
|
9990
|
-
}
|
|
9991
|
-
if (!paymentHeader) {
|
|
9992
|
-
throw new PaymentError("402 response but no payment requirements found");
|
|
9993
|
-
}
|
|
9994
|
-
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
9995
|
-
const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
|
|
9996
|
-
if (!details.network?.startsWith("solana:")) {
|
|
9997
|
-
throw new PaymentError(
|
|
9998
|
-
`Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
|
|
9999
|
-
);
|
|
10000
|
-
}
|
|
10001
|
-
const feePayer = details.extra?.feePayer;
|
|
10002
|
-
if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
|
|
10003
|
-
const fromAddress = await this.getWalletAddress();
|
|
10004
|
-
const secretKey = await solanaKeyToBytes(this.privateKey);
|
|
10005
|
-
const extensions = paymentRequired.extensions;
|
|
10006
|
-
const paymentPayload = await createSolanaPaymentPayload(
|
|
10007
|
-
secretKey,
|
|
10008
|
-
fromAddress,
|
|
10009
|
-
details.recipient,
|
|
10010
|
-
details.amount,
|
|
10011
|
-
feePayer,
|
|
10012
|
-
{
|
|
10013
|
-
resourceUrl: validateResourceUrl(
|
|
10014
|
-
details.resource?.url || url,
|
|
10015
|
-
this.apiUrl
|
|
10016
|
-
),
|
|
10017
|
-
resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
|
|
10018
|
-
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
|
|
10019
|
-
extra: details.extra,
|
|
10020
|
-
extensions,
|
|
10021
|
-
rpcUrl: this.rpcUrl,
|
|
10022
|
-
rpcHeaders: this.rpcHeaders,
|
|
10023
|
-
forceFreshBlockhash
|
|
10024
|
-
}
|
|
10025
|
-
);
|
|
10132
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(url, response, forceFreshBlockhash);
|
|
10026
10133
|
const retryResponse = await this.fetchWithTimeout(url, {
|
|
10027
10134
|
method: "POST",
|
|
10028
10135
|
headers: {
|
|
@@ -10032,24 +10139,8 @@ var SolanaLLMClient = class {
|
|
|
10032
10139
|
},
|
|
10033
10140
|
body: JSON.stringify(body)
|
|
10034
10141
|
});
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
throw new SafeStaleBlockhashError();
|
|
10038
|
-
}
|
|
10039
|
-
throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
|
|
10040
|
-
}
|
|
10041
|
-
if (!retryResponse.ok) {
|
|
10042
|
-
let errorBody;
|
|
10043
|
-
try {
|
|
10044
|
-
errorBody = await retryResponse.json();
|
|
10045
|
-
} catch {
|
|
10046
|
-
errorBody = { error: "Request failed" };
|
|
10047
|
-
}
|
|
10048
|
-
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
10049
|
-
}
|
|
10050
|
-
const costUsd = parseFloat(details.amount) / 1e6;
|
|
10051
|
-
this.sessionCalls += 1;
|
|
10052
|
-
this.sessionTotalUsd += costUsd;
|
|
10142
|
+
await this.assertPaid(retryResponse);
|
|
10143
|
+
this.recordSettlement(costUsd);
|
|
10053
10144
|
return retryResponse.json();
|
|
10054
10145
|
}
|
|
10055
10146
|
async getWithPaymentRaw(endpoint, params) {
|
|
@@ -10082,51 +10173,7 @@ var SolanaLLMClient = class {
|
|
|
10082
10173
|
}
|
|
10083
10174
|
}
|
|
10084
10175
|
async handleGetPaymentAndRetryRaw(url, endpoint, params, response, forceFreshBlockhash = false) {
|
|
10085
|
-
|
|
10086
|
-
if (!paymentHeader) {
|
|
10087
|
-
try {
|
|
10088
|
-
const respBody = await response.json();
|
|
10089
|
-
if (respBody.accepts || respBody.x402Version) {
|
|
10090
|
-
paymentHeader = btoa(JSON.stringify(respBody));
|
|
10091
|
-
}
|
|
10092
|
-
} catch {
|
|
10093
|
-
}
|
|
10094
|
-
}
|
|
10095
|
-
if (!paymentHeader) {
|
|
10096
|
-
throw new PaymentError("402 response but no payment requirements found");
|
|
10097
|
-
}
|
|
10098
|
-
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
10099
|
-
const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
|
|
10100
|
-
if (!details.network?.startsWith("solana:")) {
|
|
10101
|
-
throw new PaymentError(
|
|
10102
|
-
`Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
|
|
10103
|
-
);
|
|
10104
|
-
}
|
|
10105
|
-
const feePayer = details.extra?.feePayer;
|
|
10106
|
-
if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
|
|
10107
|
-
const fromAddress = await this.getWalletAddress();
|
|
10108
|
-
const secretKey = await solanaKeyToBytes(this.privateKey);
|
|
10109
|
-
const extensions = paymentRequired.extensions;
|
|
10110
|
-
const paymentPayload = await createSolanaPaymentPayload(
|
|
10111
|
-
secretKey,
|
|
10112
|
-
fromAddress,
|
|
10113
|
-
details.recipient,
|
|
10114
|
-
details.amount,
|
|
10115
|
-
feePayer,
|
|
10116
|
-
{
|
|
10117
|
-
resourceUrl: validateResourceUrl(
|
|
10118
|
-
details.resource?.url || url,
|
|
10119
|
-
this.apiUrl
|
|
10120
|
-
),
|
|
10121
|
-
resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
|
|
10122
|
-
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
|
|
10123
|
-
extra: details.extra,
|
|
10124
|
-
extensions,
|
|
10125
|
-
rpcUrl: this.rpcUrl,
|
|
10126
|
-
rpcHeaders: this.rpcHeaders,
|
|
10127
|
-
forceFreshBlockhash
|
|
10128
|
-
}
|
|
10129
|
-
);
|
|
10176
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(url, response, forceFreshBlockhash);
|
|
10130
10177
|
const query = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
10131
10178
|
const retryUrl = `${this.apiUrl}${endpoint}${query}`;
|
|
10132
10179
|
const retryResponse = await this.fetchWithTimeout(retryUrl, {
|
|
@@ -10136,25 +10183,44 @@ var SolanaLLMClient = class {
|
|
|
10136
10183
|
"PAYMENT-SIGNATURE": paymentPayload
|
|
10137
10184
|
}
|
|
10138
10185
|
});
|
|
10139
|
-
|
|
10140
|
-
|
|
10186
|
+
await this.assertPaid(retryResponse);
|
|
10187
|
+
this.recordSettlement(costUsd);
|
|
10188
|
+
return retryResponse.json();
|
|
10189
|
+
}
|
|
10190
|
+
/**
|
|
10191
|
+
* Fail a post-payment response, telling a re-signable rejection from a real one.
|
|
10192
|
+
*
|
|
10193
|
+
* A `402` here is not "pay again": it is the gateway refusing the payment we
|
|
10194
|
+
* just signed. Only a rejection the gateway attributes to the VERIFICATION
|
|
10195
|
+
* phase is safe to re-sign — anything settled, or ambiguous about which
|
|
10196
|
+
* phase it failed in, could already have moved USDC, and re-signing it would
|
|
10197
|
+
* pay twice. {@link isSafeStaleBlockhashResponse} is where that judgement
|
|
10198
|
+
* lives.
|
|
10199
|
+
* @param response - the reply to the paid request.
|
|
10200
|
+
* @throws SafeStaleBlockhashError when the caller should re-sign, PaymentError
|
|
10201
|
+
* when it should not, APIError for any other failure.
|
|
10202
|
+
*/
|
|
10203
|
+
async assertPaid(response) {
|
|
10204
|
+
if (response.status === 402) {
|
|
10205
|
+
if (await isSafeStaleBlockhashResponse(response)) {
|
|
10141
10206
|
throw new SafeStaleBlockhashError();
|
|
10142
10207
|
}
|
|
10143
10208
|
throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
|
|
10144
10209
|
}
|
|
10145
|
-
if (!
|
|
10210
|
+
if (!response.ok) {
|
|
10146
10211
|
let errorBody;
|
|
10147
10212
|
try {
|
|
10148
|
-
errorBody = await
|
|
10213
|
+
errorBody = await response.json();
|
|
10149
10214
|
} catch {
|
|
10150
10215
|
errorBody = { error: "Request failed" };
|
|
10151
10216
|
}
|
|
10152
|
-
throw new APIError(`API error after payment: ${
|
|
10217
|
+
throw new APIError(`API error after payment: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
|
|
10153
10218
|
}
|
|
10154
|
-
|
|
10219
|
+
}
|
|
10220
|
+
/** Count one settled x402 payment against the session total. */
|
|
10221
|
+
recordSettlement(costUsd) {
|
|
10155
10222
|
this.sessionCalls += 1;
|
|
10156
10223
|
this.sessionTotalUsd += costUsd;
|
|
10157
|
-
return retryResponse.json();
|
|
10158
10224
|
}
|
|
10159
10225
|
async fetchWithTimeout(url, options) {
|
|
10160
10226
|
const controller = new AbortController();
|
|
@@ -10558,7 +10624,8 @@ var OpenAI = class {
|
|
|
10558
10624
|
}
|
|
10559
10625
|
client;
|
|
10560
10626
|
constructor(options = {}) {
|
|
10561
|
-
const
|
|
10627
|
+
const aliases = [options.walletKey, options.privateKey].filter((k) => k !== void 0);
|
|
10628
|
+
const privateKey = aliases.find((k) => k !== "") ?? aliases[0];
|
|
10562
10629
|
const apiUrl = resolveApiKeyAuth({ apiKey: options.apiKey, privateKey, apiUrl: options.baseURL })?.apiUrl ?? options.baseURL ?? "https://blockrun.ai/api";
|
|
10563
10630
|
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
10564
10631
|
this.client = new LLMClient({
|
|
@@ -10617,7 +10684,9 @@ var AnthropicClient = class {
|
|
|
10617
10684
|
this._client = new Anthropic({
|
|
10618
10685
|
baseURL: this._apiUrl,
|
|
10619
10686
|
apiKey: "blockrun",
|
|
10620
|
-
fetch: this._x402Fetch.bind(this)
|
|
10687
|
+
fetch: this._x402Fetch.bind(this),
|
|
10688
|
+
// Account POSTs may already be billed when an upstream error arrives.
|
|
10689
|
+
...this.apiAuth ? { maxRetries: 0 } : {}
|
|
10621
10690
|
});
|
|
10622
10691
|
return this._client;
|
|
10623
10692
|
})();
|
|
@@ -10626,6 +10695,10 @@ var AnthropicClient = class {
|
|
|
10626
10695
|
async _x402Fetch(input, init) {
|
|
10627
10696
|
const controller = new AbortController();
|
|
10628
10697
|
const timeoutId = setTimeout(() => controller.abort(), this._timeout);
|
|
10698
|
+
const callerSignal = init?.signal ?? (input instanceof Request ? input.signal : void 0);
|
|
10699
|
+
const onAbort = () => controller.abort(callerSignal?.reason);
|
|
10700
|
+
callerSignal?.addEventListener("abort", onAbort, { once: true });
|
|
10701
|
+
if (callerSignal?.aborted) onAbort();
|
|
10629
10702
|
try {
|
|
10630
10703
|
const mergedInit = { ...init, signal: controller.signal };
|
|
10631
10704
|
if (this.apiAuth) return await this.apiAuth.fetch(input, mergedInit, false);
|
|
@@ -10679,6 +10752,7 @@ var AnthropicClient = class {
|
|
|
10679
10752
|
}
|
|
10680
10753
|
return response;
|
|
10681
10754
|
} finally {
|
|
10755
|
+
callerSignal?.removeEventListener("abort", onAbort);
|
|
10682
10756
|
clearTimeout(timeoutId);
|
|
10683
10757
|
}
|
|
10684
10758
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/llm",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TypeScript SDK for BlockRun - every frontier model behind one OpenAI-compatible client. Pay with a BlockRun API key or per-call in USDC. Smart routing picks the cheapest capable model. No rate limits.",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"dev": "tsup --watch",
|
|
23
23
|
"test": "vitest",
|
|
24
24
|
"lint": "eslint src/",
|
|
25
|
-
"typecheck": "tsc --noEmit"
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test:package": "npm run build && node scripts/check-package.mjs"
|
|
26
27
|
},
|
|
27
28
|
"keywords": [
|
|
28
29
|
"llm",
|
|
@@ -52,9 +53,7 @@
|
|
|
52
53
|
},
|
|
53
54
|
"dependencies": {
|
|
54
55
|
"bs58": "^6.0.0",
|
|
55
|
-
"viem": "^2.56.3"
|
|
56
|
-
},
|
|
57
|
-
"optionalDependencies": {
|
|
56
|
+
"viem": "^2.56.3",
|
|
58
57
|
"@anthropic-ai/sdk": "^0.123.0"
|
|
59
58
|
},
|
|
60
59
|
"devDependencies": {
|