@blockrun/llm 3.14.2 → 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 +58 -23
- 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.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
|
}
|