@blockrun/llm 3.14.3 → 3.15.1
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 +45 -10
- package/dist/index.cjs +246 -169
- package/dist/index.d.cts +101 -1
- package/dist/index.d.ts +101 -1
- package/dist/index.js +246 -169
- package/package.json +4 -5
package/dist/index.cjs
CHANGED
|
@@ -243,9 +243,12 @@ function sanitizeErrorResponse(errorBody) {
|
|
|
243
243
|
return { message: "API request failed" };
|
|
244
244
|
}
|
|
245
245
|
const body = errorBody;
|
|
246
|
+
const detail = typeof body.message === "string" && body.message !== body.error ? body.message : void 0;
|
|
246
247
|
return {
|
|
247
248
|
message: typeof body.error === "string" ? body.error : "API request failed",
|
|
248
|
-
code: typeof body.code === "string" ? body.code : void 0
|
|
249
|
+
code: typeof body.code === "string" ? body.code : void 0,
|
|
250
|
+
...detail ? { detail } : {},
|
|
251
|
+
...typeof body.hint === "string" ? { hint: body.hint } : {}
|
|
249
252
|
};
|
|
250
253
|
}
|
|
251
254
|
function validateResourceUrl(url, baseUrl) {
|
|
@@ -275,6 +278,21 @@ function validateResourceUrl(url, baseUrl) {
|
|
|
275
278
|
var TRANSIENT_STATUS = /* @__PURE__ */ new Set([502, 503, 504, 522, 524]);
|
|
276
279
|
var TRANSIENT_RETRIES = 2;
|
|
277
280
|
var TRANSIENT_BACKOFF_MS = 1e3;
|
|
281
|
+
function retryDelay(ms, signal) {
|
|
282
|
+
signal?.throwIfAborted();
|
|
283
|
+
return new Promise((resolve, reject) => {
|
|
284
|
+
const onAbort = () => {
|
|
285
|
+
clearTimeout(timer);
|
|
286
|
+
signal?.removeEventListener("abort", onAbort);
|
|
287
|
+
reject(signal?.reason);
|
|
288
|
+
};
|
|
289
|
+
const timer = setTimeout(() => {
|
|
290
|
+
signal?.removeEventListener("abort", onAbort);
|
|
291
|
+
resolve();
|
|
292
|
+
}, ms);
|
|
293
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
294
|
+
});
|
|
295
|
+
}
|
|
278
296
|
var API_KEY_URL = "https://api.blockrun.ai";
|
|
279
297
|
var PORTAL_URL = "https://user.blockrun.ai";
|
|
280
298
|
function resolveApiKeyAuth(options) {
|
|
@@ -324,6 +342,7 @@ var ApiKeyAuth = class {
|
|
|
324
342
|
headers.set("authorization", `Bearer ${this.#key}`);
|
|
325
343
|
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
326
344
|
const retries = method === "GET" || method === "HEAD" ? TRANSIENT_RETRIES : 0;
|
|
345
|
+
const signal = init?.signal ?? request?.signal;
|
|
327
346
|
let response;
|
|
328
347
|
for (let attempt = 0; ; attempt++) {
|
|
329
348
|
response = await globalThis.fetch(request ? new Request(url, request) : url, {
|
|
@@ -332,7 +351,8 @@ var ApiKeyAuth = class {
|
|
|
332
351
|
redirect: "error"
|
|
333
352
|
});
|
|
334
353
|
if (attempt >= retries || !TRANSIENT_STATUS.has(response.status)) break;
|
|
335
|
-
await
|
|
354
|
+
await response.body?.cancel();
|
|
355
|
+
await retryDelay(TRANSIENT_BACKOFF_MS * (attempt + 1), signal);
|
|
336
356
|
}
|
|
337
357
|
if (raiseErrors && !response.ok) {
|
|
338
358
|
let body;
|
|
@@ -4600,7 +4620,7 @@ function getCostSummary() {
|
|
|
4600
4620
|
}
|
|
4601
4621
|
|
|
4602
4622
|
// src/version.ts
|
|
4603
|
-
var SDK_VERSION = "3.
|
|
4623
|
+
var SDK_VERSION = "3.15.1";
|
|
4604
4624
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
4605
4625
|
|
|
4606
4626
|
// src/client.ts
|
|
@@ -6861,7 +6881,7 @@ var VideoClient = class {
|
|
|
6861
6881
|
* Submits an async job, then polls until the video is ready. Typical total
|
|
6862
6882
|
* wall-time is 60-180s, but upstream status can lag several minutes behind
|
|
6863
6883
|
* actual completion. If upstream runs past the budget (default 15min),
|
|
6864
|
-
* throws
|
|
6884
|
+
* throws with the existing poll URL; check billing before submitting again.
|
|
6865
6885
|
*
|
|
6866
6886
|
* @param prompt - Text description of the video
|
|
6867
6887
|
* @param options - Optional generation parameters
|
|
@@ -7061,7 +7081,7 @@ var VideoClient = class {
|
|
|
7061
7081
|
}
|
|
7062
7082
|
}
|
|
7063
7083
|
throw new APIError(
|
|
7064
|
-
`Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}).
|
|
7084
|
+
`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
7085
|
504,
|
|
7066
7086
|
{ id: submitData.id, last_status: lastStatus, poll_url: pollUrl }
|
|
7067
7087
|
);
|
|
@@ -8553,6 +8573,35 @@ var RpcClient = class {
|
|
|
8553
8573
|
}
|
|
8554
8574
|
};
|
|
8555
8575
|
|
|
8576
|
+
// src/sse.ts
|
|
8577
|
+
async function* readSseFrames(response, onMissingBody) {
|
|
8578
|
+
if (!response.body) throw onMissingBody(response.status);
|
|
8579
|
+
const reader = response.body.getReader();
|
|
8580
|
+
const decoder = new TextDecoder();
|
|
8581
|
+
let buffer = "";
|
|
8582
|
+
try {
|
|
8583
|
+
while (true) {
|
|
8584
|
+
const { done, value } = await reader.read();
|
|
8585
|
+
if (done) break;
|
|
8586
|
+
buffer += decoder.decode(value, { stream: true });
|
|
8587
|
+
const lines = buffer.split("\n");
|
|
8588
|
+
buffer = lines.pop() || "";
|
|
8589
|
+
for (const line of lines) {
|
|
8590
|
+
const trimmed = line.trim();
|
|
8591
|
+
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
8592
|
+
const data = trimmed.slice(6);
|
|
8593
|
+
if (data === "[DONE]") return;
|
|
8594
|
+
try {
|
|
8595
|
+
yield JSON.parse(data);
|
|
8596
|
+
} catch {
|
|
8597
|
+
}
|
|
8598
|
+
}
|
|
8599
|
+
}
|
|
8600
|
+
} finally {
|
|
8601
|
+
reader.releaseLock();
|
|
8602
|
+
}
|
|
8603
|
+
}
|
|
8604
|
+
|
|
8556
8605
|
// src/blockrun.ts
|
|
8557
8606
|
var import_accounts14 = require("viem/accounts");
|
|
8558
8607
|
var DEFAULT_API_URL13 = "https://blockrun.ai/api";
|
|
@@ -8762,33 +8811,10 @@ var BlockrunClient = class {
|
|
|
8762
8811
|
await this.throwApiError(resp402, `stream failed (${url})`);
|
|
8763
8812
|
return;
|
|
8764
8813
|
}
|
|
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
|
-
}
|
|
8814
|
+
yield* readSseFrames(
|
|
8815
|
+
streamResp,
|
|
8816
|
+
(status2) => new APIError("Stream response has no body", status2, {})
|
|
8817
|
+
);
|
|
8792
8818
|
}
|
|
8793
8819
|
// --------------------------------------------------------------------
|
|
8794
8820
|
// Internal: shared infrastructure
|
|
@@ -9719,6 +9745,123 @@ var SolanaLLMClient = class {
|
|
|
9719
9745
|
response.routing = decision;
|
|
9720
9746
|
return { response, model: decision.model, routing: decision };
|
|
9721
9747
|
}
|
|
9748
|
+
/**
|
|
9749
|
+
* Stream a Server-Sent Events endpoint, paid on Solana.
|
|
9750
|
+
*
|
|
9751
|
+
* The Solana counterpart to `BlockrunClient.stream`, and the reason it had to
|
|
9752
|
+
* exist: a streaming harness cannot use this client at all without it, so
|
|
9753
|
+
* "BlockRun supports Solana" stopped being true the moment a caller streamed.
|
|
9754
|
+
* `chatCompletion` buffers the whole answer, which is the wrong shape for an
|
|
9755
|
+
* agent loop and for anything that shows tokens as they arrive.
|
|
9756
|
+
*
|
|
9757
|
+
* The handshake is the one the non-streaming paths use — `402`, sign an SPL
|
|
9758
|
+
* TransferChecked authorization locally, replay with `PAYMENT-SIGNATURE` —
|
|
9759
|
+
* with the same verification-phase re-sign on a stale blockhash. What differs
|
|
9760
|
+
* is that the paid response is not read as JSON: it is handed to the SSE
|
|
9761
|
+
* reader with its body untouched.
|
|
9762
|
+
*
|
|
9763
|
+
* A `200` on the first request is returned as-is and settles nothing. That is
|
|
9764
|
+
* the free tier (the gateway answers a `billing_mode: "free"` model without a
|
|
9765
|
+
* 402 at all) and it is also API-key mode, where billing is on the account
|
|
9766
|
+
* rather than on a wallet.
|
|
9767
|
+
*
|
|
9768
|
+
* Yields each `data:` frame parsed as JSON, and stops at `data: [DONE]`.
|
|
9769
|
+
* Malformed frames are skipped rather than thrown — see {@link readSseFrames}.
|
|
9770
|
+
*
|
|
9771
|
+
* @example
|
|
9772
|
+
* for await (const chunk of client.stream<ChatChunk>("/v1/chat/completions", {
|
|
9773
|
+
* model: "deepseek/deepseek-chat",
|
|
9774
|
+
* messages: [{ role: "user", content: "Hi" }],
|
|
9775
|
+
* stream: true,
|
|
9776
|
+
* })) {
|
|
9777
|
+
* process.stdout.write(chunk.choices?.[0]?.delta?.content ?? "");
|
|
9778
|
+
* }
|
|
9779
|
+
*
|
|
9780
|
+
* @param path - endpoint after the API root; a leading `/api` is tolerated.
|
|
9781
|
+
* @param body - JSON request body. Set `stream: true` yourself — this method
|
|
9782
|
+
* does not inject it, because the gateway prices a streaming and a
|
|
9783
|
+
* non-streaming request the same and silently rewriting a caller's body is
|
|
9784
|
+
* how you end up debugging a request you did not send.
|
|
9785
|
+
* @returns each decoded SSE frame, in order.
|
|
9786
|
+
*/
|
|
9787
|
+
async *stream(path6, body) {
|
|
9788
|
+
const url = this.buildUrl(path6);
|
|
9789
|
+
const response = await this.openPaidStream(url, JSON.stringify(body ?? {}));
|
|
9790
|
+
yield* readSseFrames(
|
|
9791
|
+
response,
|
|
9792
|
+
(status2) => new APIError("Stream response has no body", status2, {})
|
|
9793
|
+
);
|
|
9794
|
+
}
|
|
9795
|
+
/**
|
|
9796
|
+
* Get to a streaming response, paying for it if the gateway asks.
|
|
9797
|
+
*
|
|
9798
|
+
* Separate from {@link SolanaLLMClient.stream} because a generator cannot
|
|
9799
|
+
* retry cleanly around a `yield`: the stale-blockhash re-sign has to finish
|
|
9800
|
+
* before the first frame is handed out, and putting the loop here keeps the
|
|
9801
|
+
* payment decision entirely ahead of any output the caller has seen.
|
|
9802
|
+
*
|
|
9803
|
+
* @param url - resolved endpoint URL.
|
|
9804
|
+
* @param requestBody - the serialized body, reused verbatim on the paid retry
|
|
9805
|
+
* so the gateway prices and answers the same request it quoted for.
|
|
9806
|
+
* @returns a response whose body has not been read.
|
|
9807
|
+
*/
|
|
9808
|
+
async openPaidStream(url, requestBody) {
|
|
9809
|
+
for (let staleRetries = 0; ; ) {
|
|
9810
|
+
const response = await this.fetchWithTimeout(url, {
|
|
9811
|
+
method: "POST",
|
|
9812
|
+
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
9813
|
+
body: requestBody
|
|
9814
|
+
});
|
|
9815
|
+
if (response.ok) return response;
|
|
9816
|
+
if (response.status !== 402) {
|
|
9817
|
+
let errorBody;
|
|
9818
|
+
try {
|
|
9819
|
+
errorBody = await response.json();
|
|
9820
|
+
} catch {
|
|
9821
|
+
errorBody = { error: "Request failed" };
|
|
9822
|
+
}
|
|
9823
|
+
throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
|
|
9824
|
+
}
|
|
9825
|
+
try {
|
|
9826
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(
|
|
9827
|
+
url,
|
|
9828
|
+
response,
|
|
9829
|
+
staleRetries > 0
|
|
9830
|
+
);
|
|
9831
|
+
const paid = await this.fetchWithTimeout(url, {
|
|
9832
|
+
method: "POST",
|
|
9833
|
+
headers: {
|
|
9834
|
+
"Content-Type": "application/json",
|
|
9835
|
+
"User-Agent": USER_AGENT,
|
|
9836
|
+
"PAYMENT-SIGNATURE": paymentPayload
|
|
9837
|
+
},
|
|
9838
|
+
body: requestBody
|
|
9839
|
+
});
|
|
9840
|
+
await this.assertPaid(paid);
|
|
9841
|
+
this.recordSettlement(costUsd);
|
|
9842
|
+
return paid;
|
|
9843
|
+
} catch (error) {
|
|
9844
|
+
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
9845
|
+
await waitForStaleRetry(staleRetries++);
|
|
9846
|
+
continue;
|
|
9847
|
+
}
|
|
9848
|
+
}
|
|
9849
|
+
}
|
|
9850
|
+
/**
|
|
9851
|
+
* Resolve an endpoint path against this client's API root.
|
|
9852
|
+
*
|
|
9853
|
+
* A leading `/api` is stripped for the same reason `BlockrunClient` strips
|
|
9854
|
+
* it: the documented paths are written `/api/v1/…` on the website and
|
|
9855
|
+
* `/v1/…` in this SDK, and a caller who copies one into the other should get
|
|
9856
|
+
* their request rather than a 404.
|
|
9857
|
+
* @param path - endpoint path, with or without a leading slash.
|
|
9858
|
+
* @returns the absolute URL to call.
|
|
9859
|
+
*/
|
|
9860
|
+
buildUrl(path6) {
|
|
9861
|
+
let normalized = path6.startsWith("/") ? path6 : `/${path6}`;
|
|
9862
|
+
if (normalized.startsWith("/api/")) normalized = normalized.slice(4);
|
|
9863
|
+
return `${this.apiUrl}${normalized}`;
|
|
9864
|
+
}
|
|
9722
9865
|
/** List available models. */
|
|
9723
9866
|
async listModels() {
|
|
9724
9867
|
const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, { method: "GET" });
|
|
@@ -9989,7 +10132,25 @@ var SolanaLLMClient = class {
|
|
|
9989
10132
|
return response.json();
|
|
9990
10133
|
}
|
|
9991
10134
|
}
|
|
9992
|
-
|
|
10135
|
+
/**
|
|
10136
|
+
* Turn a `402` into a signed Solana payment payload.
|
|
10137
|
+
*
|
|
10138
|
+
* Extracted because four call sites need it — chat, the raw POST helpers, the
|
|
10139
|
+
* raw GET helper, and {@link SolanaLLMClient.stream} — and it had been
|
|
10140
|
+
* written out three times before this. That mattered more than ordinary
|
|
10141
|
+
* duplication: this is the code that signs a transfer of the caller's USDC,
|
|
10142
|
+
* so three copies meant every fix to it had to be applied three times or
|
|
10143
|
+
* quietly apply to two thirds of the paths.
|
|
10144
|
+
*
|
|
10145
|
+
* @param url - the request being paid for.
|
|
10146
|
+
* @param response - the gateway's `402`, not yet consumed.
|
|
10147
|
+
* @param forceFreshBlockhash - set on a re-sign after a stale-blockhash
|
|
10148
|
+
* rejection, so the retry cannot produce byte-identical transaction bytes.
|
|
10149
|
+
* @param resourceFallback - resource URL to claim when the 402 states none.
|
|
10150
|
+
* @returns the header value to replay with, and what it will settle for.
|
|
10151
|
+
* @throws PaymentError when the 402 carries no usable Solana requirements.
|
|
10152
|
+
*/
|
|
10153
|
+
async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
|
|
9993
10154
|
let paymentHeader = response.headers.get("payment-required");
|
|
9994
10155
|
if (!paymentHeader) {
|
|
9995
10156
|
try {
|
|
@@ -10023,7 +10184,7 @@ var SolanaLLMClient = class {
|
|
|
10023
10184
|
feePayer,
|
|
10024
10185
|
{
|
|
10025
10186
|
resourceUrl: validateResourceUrl(
|
|
10026
|
-
details.resource?.url ||
|
|
10187
|
+
details.resource?.url || resourceFallback,
|
|
10027
10188
|
this.apiUrl
|
|
10028
10189
|
),
|
|
10029
10190
|
resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
|
|
@@ -10035,6 +10196,15 @@ var SolanaLLMClient = class {
|
|
|
10035
10196
|
forceFreshBlockhash
|
|
10036
10197
|
}
|
|
10037
10198
|
);
|
|
10199
|
+
return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
|
|
10200
|
+
}
|
|
10201
|
+
async handlePaymentAndRetry(url, body, response, forceFreshBlockhash = false) {
|
|
10202
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(
|
|
10203
|
+
url,
|
|
10204
|
+
response,
|
|
10205
|
+
forceFreshBlockhash,
|
|
10206
|
+
`${this.apiUrl}/v1/chat/completions`
|
|
10207
|
+
);
|
|
10038
10208
|
const retryResponse = await this.fetchWithTimeout(url, {
|
|
10039
10209
|
method: "POST",
|
|
10040
10210
|
headers: {
|
|
@@ -10044,24 +10214,8 @@ var SolanaLLMClient = class {
|
|
|
10044
10214
|
},
|
|
10045
10215
|
body: JSON.stringify(body)
|
|
10046
10216
|
});
|
|
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;
|
|
10217
|
+
await this.assertPaid(retryResponse);
|
|
10218
|
+
this.recordSettlement(costUsd);
|
|
10065
10219
|
return retryResponse.json();
|
|
10066
10220
|
}
|
|
10067
10221
|
async requestWithPaymentRaw(endpoint, body) {
|
|
@@ -10095,51 +10249,7 @@ var SolanaLLMClient = class {
|
|
|
10095
10249
|
}
|
|
10096
10250
|
}
|
|
10097
10251
|
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
|
-
);
|
|
10252
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(url, response, forceFreshBlockhash);
|
|
10143
10253
|
const retryResponse = await this.fetchWithTimeout(url, {
|
|
10144
10254
|
method: "POST",
|
|
10145
10255
|
headers: {
|
|
@@ -10149,24 +10259,8 @@ var SolanaLLMClient = class {
|
|
|
10149
10259
|
},
|
|
10150
10260
|
body: JSON.stringify(body)
|
|
10151
10261
|
});
|
|
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;
|
|
10262
|
+
await this.assertPaid(retryResponse);
|
|
10263
|
+
this.recordSettlement(costUsd);
|
|
10170
10264
|
return retryResponse.json();
|
|
10171
10265
|
}
|
|
10172
10266
|
async getWithPaymentRaw(endpoint, params) {
|
|
@@ -10199,51 +10293,7 @@ var SolanaLLMClient = class {
|
|
|
10199
10293
|
}
|
|
10200
10294
|
}
|
|
10201
10295
|
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
|
-
);
|
|
10296
|
+
const { paymentPayload, costUsd } = await this.signPaymentFrom402(url, response, forceFreshBlockhash);
|
|
10247
10297
|
const query = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
10248
10298
|
const retryUrl = `${this.apiUrl}${endpoint}${query}`;
|
|
10249
10299
|
const retryResponse = await this.fetchWithTimeout(retryUrl, {
|
|
@@ -10253,25 +10303,44 @@ var SolanaLLMClient = class {
|
|
|
10253
10303
|
"PAYMENT-SIGNATURE": paymentPayload
|
|
10254
10304
|
}
|
|
10255
10305
|
});
|
|
10256
|
-
|
|
10257
|
-
|
|
10306
|
+
await this.assertPaid(retryResponse);
|
|
10307
|
+
this.recordSettlement(costUsd);
|
|
10308
|
+
return retryResponse.json();
|
|
10309
|
+
}
|
|
10310
|
+
/**
|
|
10311
|
+
* Fail a post-payment response, telling a re-signable rejection from a real one.
|
|
10312
|
+
*
|
|
10313
|
+
* A `402` here is not "pay again": it is the gateway refusing the payment we
|
|
10314
|
+
* just signed. Only a rejection the gateway attributes to the VERIFICATION
|
|
10315
|
+
* phase is safe to re-sign — anything settled, or ambiguous about which
|
|
10316
|
+
* phase it failed in, could already have moved USDC, and re-signing it would
|
|
10317
|
+
* pay twice. {@link isSafeStaleBlockhashResponse} is where that judgement
|
|
10318
|
+
* lives.
|
|
10319
|
+
* @param response - the reply to the paid request.
|
|
10320
|
+
* @throws SafeStaleBlockhashError when the caller should re-sign, PaymentError
|
|
10321
|
+
* when it should not, APIError for any other failure.
|
|
10322
|
+
*/
|
|
10323
|
+
async assertPaid(response) {
|
|
10324
|
+
if (response.status === 402) {
|
|
10325
|
+
if (await isSafeStaleBlockhashResponse(response)) {
|
|
10258
10326
|
throw new SafeStaleBlockhashError();
|
|
10259
10327
|
}
|
|
10260
10328
|
throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
|
|
10261
10329
|
}
|
|
10262
|
-
if (!
|
|
10330
|
+
if (!response.ok) {
|
|
10263
10331
|
let errorBody;
|
|
10264
10332
|
try {
|
|
10265
|
-
errorBody = await
|
|
10333
|
+
errorBody = await response.json();
|
|
10266
10334
|
} catch {
|
|
10267
10335
|
errorBody = { error: "Request failed" };
|
|
10268
10336
|
}
|
|
10269
|
-
throw new APIError(`API error after payment: ${
|
|
10337
|
+
throw new APIError(`API error after payment: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
|
|
10270
10338
|
}
|
|
10271
|
-
|
|
10339
|
+
}
|
|
10340
|
+
/** Count one settled x402 payment against the session total. */
|
|
10341
|
+
recordSettlement(costUsd) {
|
|
10272
10342
|
this.sessionCalls += 1;
|
|
10273
10343
|
this.sessionTotalUsd += costUsd;
|
|
10274
|
-
return retryResponse.json();
|
|
10275
10344
|
}
|
|
10276
10345
|
async fetchWithTimeout(url, options) {
|
|
10277
10346
|
const controller = new AbortController();
|
|
@@ -10675,7 +10744,8 @@ var OpenAI = class {
|
|
|
10675
10744
|
}
|
|
10676
10745
|
client;
|
|
10677
10746
|
constructor(options = {}) {
|
|
10678
|
-
const
|
|
10747
|
+
const aliases = [options.walletKey, options.privateKey].filter((k) => k !== void 0);
|
|
10748
|
+
const privateKey = aliases.find((k) => k !== "") ?? aliases[0];
|
|
10679
10749
|
const apiUrl = resolveApiKeyAuth({ apiKey: options.apiKey, privateKey, apiUrl: options.baseURL })?.apiUrl ?? options.baseURL ?? "https://blockrun.ai/api";
|
|
10680
10750
|
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
10681
10751
|
this.client = new LLMClient({
|
|
@@ -10734,7 +10804,9 @@ var AnthropicClient = class {
|
|
|
10734
10804
|
this._client = new Anthropic({
|
|
10735
10805
|
baseURL: this._apiUrl,
|
|
10736
10806
|
apiKey: "blockrun",
|
|
10737
|
-
fetch: this._x402Fetch.bind(this)
|
|
10807
|
+
fetch: this._x402Fetch.bind(this),
|
|
10808
|
+
// Account POSTs may already be billed when an upstream error arrives.
|
|
10809
|
+
...this.apiAuth ? { maxRetries: 0 } : {}
|
|
10738
10810
|
});
|
|
10739
10811
|
return this._client;
|
|
10740
10812
|
})();
|
|
@@ -10743,6 +10815,10 @@ var AnthropicClient = class {
|
|
|
10743
10815
|
async _x402Fetch(input, init) {
|
|
10744
10816
|
const controller = new AbortController();
|
|
10745
10817
|
const timeoutId = setTimeout(() => controller.abort(), this._timeout);
|
|
10818
|
+
const callerSignal = init?.signal ?? (input instanceof Request ? input.signal : void 0);
|
|
10819
|
+
const onAbort = () => controller.abort(callerSignal?.reason);
|
|
10820
|
+
callerSignal?.addEventListener("abort", onAbort, { once: true });
|
|
10821
|
+
if (callerSignal?.aborted) onAbort();
|
|
10746
10822
|
try {
|
|
10747
10823
|
const mergedInit = { ...init, signal: controller.signal };
|
|
10748
10824
|
if (this.apiAuth) return await this.apiAuth.fetch(input, mergedInit, false);
|
|
@@ -10796,6 +10872,7 @@ var AnthropicClient = class {
|
|
|
10796
10872
|
}
|
|
10797
10873
|
return response;
|
|
10798
10874
|
} finally {
|
|
10875
|
+
callerSignal?.removeEventListener("abort", onAbort);
|
|
10799
10876
|
clearTimeout(timeoutId);
|
|
10800
10877
|
}
|
|
10801
10878
|
}
|