@blockrun/llm 3.17.1 → 3.19.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 +134 -16
- package/dist/index.cjs +534 -97
- package/dist/index.d.cts +113 -11
- package/dist/index.d.ts +113 -11
- package/dist/index.js +537 -100
- package/package.json +19 -4
package/dist/index.cjs
CHANGED
|
@@ -284,7 +284,7 @@ var TRANSIENT_RETRIES = 2;
|
|
|
284
284
|
var TRANSIENT_BACKOFF_MS = 1e3;
|
|
285
285
|
function retryDelay(ms, signal) {
|
|
286
286
|
signal?.throwIfAborted();
|
|
287
|
-
return new Promise((
|
|
287
|
+
return new Promise((resolve2, reject) => {
|
|
288
288
|
const onAbort = () => {
|
|
289
289
|
clearTimeout(timer);
|
|
290
290
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -292,7 +292,7 @@ function retryDelay(ms, signal) {
|
|
|
292
292
|
};
|
|
293
293
|
const timer = setTimeout(() => {
|
|
294
294
|
signal?.removeEventListener("abort", onAbort);
|
|
295
|
-
|
|
295
|
+
resolve2();
|
|
296
296
|
}, ms);
|
|
297
297
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
298
298
|
});
|
|
@@ -328,8 +328,8 @@ var ApiKeyAuth = class {
|
|
|
328
328
|
this.#key = key;
|
|
329
329
|
}
|
|
330
330
|
/** Resolve gateway poll paths without forwarding credentials to another origin. */
|
|
331
|
-
resolveUrl(
|
|
332
|
-
const url = new URL(
|
|
331
|
+
resolveUrl(path7) {
|
|
332
|
+
const url = new URL(path7, this.apiUrl + "/");
|
|
333
333
|
if (url.origin !== new URL(this.apiUrl).origin || url.username || url.password) {
|
|
334
334
|
throw new Error("Refusing to send a BlockRun API key to a different origin.");
|
|
335
335
|
}
|
|
@@ -395,7 +395,7 @@ var ApiKeyAuth = class {
|
|
|
395
395
|
if (["failed", "cancelled", "canceled"].includes(String(data.status))) {
|
|
396
396
|
throw new APIError("Account API job failed or was cancelled", 502, sanitizeErrorResponse(data));
|
|
397
397
|
}
|
|
398
|
-
await new Promise((
|
|
398
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.min(interval, Math.max(0, deadline - Date.now()))));
|
|
399
399
|
const remaining = deadline - Date.now();
|
|
400
400
|
if (remaining <= 0) break;
|
|
401
401
|
try {
|
|
@@ -4660,7 +4660,7 @@ function getCostSummary() {
|
|
|
4660
4660
|
}
|
|
4661
4661
|
|
|
4662
4662
|
// src/version.ts
|
|
4663
|
-
var SDK_VERSION = "3.
|
|
4663
|
+
var SDK_VERSION = "3.19.0";
|
|
4664
4664
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
4665
4665
|
|
|
4666
4666
|
// src/client.ts
|
|
@@ -5769,8 +5769,8 @@ var LLMClient = class _LLMClient {
|
|
|
5769
5769
|
* @example
|
|
5770
5770
|
* const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
|
|
5771
5771
|
*/
|
|
5772
|
-
async exa(
|
|
5773
|
-
return this.requestWithPaymentRaw(`/v1/exa/${
|
|
5772
|
+
async exa(path7, body) {
|
|
5773
|
+
return this.requestWithPaymentRaw(`/v1/exa/${path7}`, body);
|
|
5774
5774
|
}
|
|
5775
5775
|
/**
|
|
5776
5776
|
* Neural web search via Exa. Returns semantically relevant URLs and metadata.
|
|
@@ -5918,8 +5918,8 @@ var LLMClient = class _LLMClient {
|
|
|
5918
5918
|
* const market = await client.pm("kalshi/markets/KXBTC-25MAR14");
|
|
5919
5919
|
* const results = await client.pm("polymarket/search", { q: "bitcoin" });
|
|
5920
5920
|
*/
|
|
5921
|
-
async pm(
|
|
5922
|
-
return this.getWithPaymentRaw(`/v1/pm/${
|
|
5921
|
+
async pm(path7, params) {
|
|
5922
|
+
return this.getWithPaymentRaw(`/v1/pm/${path7}`, params);
|
|
5923
5923
|
}
|
|
5924
5924
|
/**
|
|
5925
5925
|
* Structured query for Predexon prediction market data (POST endpoints).
|
|
@@ -5935,8 +5935,8 @@ var LLMClient = class _LLMClient {
|
|
|
5935
5935
|
* addresses: ["0xabc...", "0xdef..."],
|
|
5936
5936
|
* });
|
|
5937
5937
|
*/
|
|
5938
|
-
async pmQuery(
|
|
5939
|
-
return this.requestWithPaymentRaw(`/v1/pm/${
|
|
5938
|
+
async pmQuery(path7, query) {
|
|
5939
|
+
return this.requestWithPaymentRaw(`/v1/pm/${path7}`, query);
|
|
5940
5940
|
}
|
|
5941
5941
|
// ── PM convenience helpers (Predexon v2) ─────────────────────────────────
|
|
5942
5942
|
// Thin wrappers over pm() / pmQuery() for the most common v2 endpoints.
|
|
@@ -6024,8 +6024,8 @@ var LLMClient = class _LLMClient {
|
|
|
6024
6024
|
* "prices/coingecko:bitcoin,base:0x..."
|
|
6025
6025
|
* @param params - Query parameters passed through to DefiLlama
|
|
6026
6026
|
*/
|
|
6027
|
-
async defi(
|
|
6028
|
-
return this.getWithPaymentRaw(`/v1/defillama/${
|
|
6027
|
+
async defi(path7, params) {
|
|
6028
|
+
return this.getWithPaymentRaw(`/v1/defillama/${path7}`, params);
|
|
6029
6029
|
}
|
|
6030
6030
|
/** All DeFi protocols with TVL. */
|
|
6031
6031
|
async defiProtocols() {
|
|
@@ -6061,9 +6061,9 @@ var LLMClient = class _LLMClient {
|
|
|
6061
6061
|
* @param params - Query params (chainId, sellToken, buyToken, sellAmount, taker, ...)
|
|
6062
6062
|
* @param body - JSON body — pass to switch to POST (gasless/submit only)
|
|
6063
6063
|
*/
|
|
6064
|
-
async dex(
|
|
6065
|
-
if (body) return this.requestWithPaymentRaw(`/v1/zerox/${
|
|
6066
|
-
return this.getWithPaymentRaw(`/v1/zerox/${
|
|
6064
|
+
async dex(path7, params, body) {
|
|
6065
|
+
if (body) return this.requestWithPaymentRaw(`/v1/zerox/${path7}`, body);
|
|
6066
|
+
return this.getWithPaymentRaw(`/v1/zerox/${path7}`, params);
|
|
6067
6067
|
}
|
|
6068
6068
|
/** Indicative Permit2 swap price — no commitment (free). */
|
|
6069
6069
|
async dexPrice(params) {
|
|
@@ -6107,8 +6107,8 @@ var LLMClient = class _LLMClient {
|
|
|
6107
6107
|
* "sandbox/status", "sandbox/terminate"
|
|
6108
6108
|
* @param body - JSON body for the endpoint
|
|
6109
6109
|
*/
|
|
6110
|
-
async modal(
|
|
6111
|
-
return this.requestWithPaymentRaw(`/v1/modal/${
|
|
6110
|
+
async modal(path7, body) {
|
|
6111
|
+
return this.requestWithPaymentRaw(`/v1/modal/${path7}`, body ?? {});
|
|
6112
6112
|
}
|
|
6113
6113
|
/** Create a sandboxed compute environment (CPU / GPU tiers). */
|
|
6114
6114
|
async modalSandboxCreate(body) {
|
|
@@ -6416,7 +6416,7 @@ var ImageClient = class {
|
|
|
6416
6416
|
const pollUrl = pollPath.startsWith("/api/") ? `${this.apiUrl.replace(/\/api$/, "")}${pollPath}` : `${this.apiUrl}${pollPath.startsWith("/") ? "" : "/"}${pollPath}`;
|
|
6417
6417
|
const startedAt = Date.now();
|
|
6418
6418
|
while (Date.now() - startedAt < POLL_MAX_DURATION_MS) {
|
|
6419
|
-
await new Promise((
|
|
6419
|
+
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
6420
6420
|
const pollResp = await this.fetchWithTimeout(pollUrl, {
|
|
6421
6421
|
method: "GET",
|
|
6422
6422
|
headers: {
|
|
@@ -6958,8 +6958,36 @@ var VideoClient = class {
|
|
|
6958
6958
|
"referenceImageUrls is mutually exclusive with imageUrl, lastFrameUrl, and realFaceAssetId."
|
|
6959
6959
|
);
|
|
6960
6960
|
}
|
|
6961
|
-
|
|
6962
|
-
|
|
6961
|
+
const imageLimit = options.model?.replace(/^bytedance\//, "") === "seedance-2.5" ? 30 : 9;
|
|
6962
|
+
if (options.referenceImageUrls.length > imageLimit) {
|
|
6963
|
+
throw new Error(`referenceImageUrls accepts at most ${imageLimit} images.`);
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
for (const [field, clips] of [
|
|
6967
|
+
["referenceVideos", options?.referenceVideos],
|
|
6968
|
+
["referenceAudios", options?.referenceAudios]
|
|
6969
|
+
]) {
|
|
6970
|
+
if (clips === void 0) continue;
|
|
6971
|
+
if (clips.length < 1 || clips.length > 3) {
|
|
6972
|
+
throw new Error(`${field} accepts 1 to 3 clips.`);
|
|
6973
|
+
}
|
|
6974
|
+
for (const clip of clips) {
|
|
6975
|
+
if (typeof clip?.url !== "string" || !/^https?:\/\//i.test(clip.url)) {
|
|
6976
|
+
throw new Error(`${field} URLs must be http(s).`);
|
|
6977
|
+
}
|
|
6978
|
+
if (clip.role !== void 0 && clip.role !== "reference") {
|
|
6979
|
+
throw new Error(`${field} role must be "reference" (or omitted).`);
|
|
6980
|
+
}
|
|
6981
|
+
}
|
|
6982
|
+
}
|
|
6983
|
+
if (options?.referenceVideos?.length || options?.referenceAudios?.length) {
|
|
6984
|
+
if (options.imageUrl || options.lastFrameUrl || options.realFaceAssetId) {
|
|
6985
|
+
throw new Error(
|
|
6986
|
+
"referenceVideos / referenceAudios are mutually exclusive with imageUrl, lastFrameUrl, and realFaceAssetId; combine them with referenceImageUrls instead."
|
|
6987
|
+
);
|
|
6988
|
+
}
|
|
6989
|
+
if (options.referenceAudios?.length && !options.referenceImageUrls?.length && !options.referenceVideos?.length) {
|
|
6990
|
+
throw new Error("referenceAudios requires referenceImageUrls or referenceVideos.");
|
|
6963
6991
|
}
|
|
6964
6992
|
}
|
|
6965
6993
|
const body = {
|
|
@@ -6976,7 +7004,14 @@ var VideoClient = class {
|
|
|
6976
7004
|
if (options?.generateAudio !== void 0) body.generate_audio = options.generateAudio;
|
|
6977
7005
|
if (options?.seed !== void 0) body.seed = options.seed;
|
|
6978
7006
|
if (options?.watermark !== void 0) body.watermark = options.watermark;
|
|
6979
|
-
if (options?.returnLastFrame) body.return_last_frame =
|
|
7007
|
+
if (options?.returnLastFrame !== void 0) body.return_last_frame = options.returnLastFrame;
|
|
7008
|
+
if (options?.referenceVideos !== void 0) body.reference_videos = options.referenceVideos;
|
|
7009
|
+
if (options?.referenceAudios !== void 0) body.reference_audios = options.referenceAudios;
|
|
7010
|
+
if (options?.bitrateMode !== void 0) body.bitrate_mode = options.bitrateMode;
|
|
7011
|
+
if (options?.outputFormat !== void 0) body.output_format = options.outputFormat;
|
|
7012
|
+
if (options?.cameraFixed !== void 0) body.camera_fixed = options.cameraFixed;
|
|
7013
|
+
if (options?.safetyIdentifier !== void 0) body.safety_identifier = options.safetyIdentifier;
|
|
7014
|
+
if (options?.inputType !== void 0) body.input_type = options.inputType;
|
|
6980
7015
|
const budgetMs = options?.budgetMs ?? DEFAULT_GENERATE_BUDGET_MS;
|
|
6981
7016
|
return this.submitAndPoll(body, budgetMs);
|
|
6982
7017
|
}
|
|
@@ -6984,8 +7019,11 @@ var VideoClient = class {
|
|
|
6984
7019
|
* Generate a video from a standard Seedance `content[]` body.
|
|
6985
7020
|
*
|
|
6986
7021
|
* Targets the gateway's `POST /v1/videos` endpoint, which accepts the
|
|
6987
|
-
* mainstream multimodal `content` array
|
|
6988
|
-
*
|
|
7022
|
+
* mainstream multimodal `content` array used by other Seedance APIs. Items
|
|
7023
|
+
* may carry a `role` — `first_frame`, `last_frame`, `reference_image`,
|
|
7024
|
+
* `reference_video`, `reference_audio` — which the gateway maps to the same
|
|
7025
|
+
* validated fields as {@link generate}; a role-less single image keeps its
|
|
7026
|
+
* first-frame meaning — so callers already holding a
|
|
6989
7027
|
* `content[]`-shaped request can submit it unchanged. The gateway validates
|
|
6990
7028
|
* unsupported inputs *before* charging, then delegates to the same x402
|
|
6991
7029
|
* submit+poll pipeline as {@link generate}.
|
|
@@ -6999,7 +7037,8 @@ var VideoClient = class {
|
|
|
6999
7037
|
* `{ type: "image_url", image_url: { url: "https://..." } }`.
|
|
7000
7038
|
* @param options - `model`, `budgetMs`, plus the same camelCase render options
|
|
7001
7039
|
* as {@link generate} (`durationSeconds`, `aspectRatio`, `resolution`,
|
|
7002
|
-
* `generateAudio`, `seed`, `watermark`, `returnLastFrame`
|
|
7040
|
+
* `generateAudio`, `seed`, `watermark`, `returnLastFrame`, `bitrateMode`,
|
|
7041
|
+
* `outputFormat`, `cameraFixed`, `safetyIdentifier`, `inputType`). These are mapped
|
|
7003
7042
|
* to the gateway's snake_case fields for you. Any other keys you pass are
|
|
7004
7043
|
* forwarded verbatim (use snake_case for those, since the gateway reads
|
|
7005
7044
|
* snake_case only).
|
|
@@ -7018,6 +7057,11 @@ var VideoClient = class {
|
|
|
7018
7057
|
seed,
|
|
7019
7058
|
watermark,
|
|
7020
7059
|
returnLastFrame,
|
|
7060
|
+
bitrateMode,
|
|
7061
|
+
outputFormat,
|
|
7062
|
+
cameraFixed,
|
|
7063
|
+
safetyIdentifier,
|
|
7064
|
+
inputType,
|
|
7021
7065
|
...extra
|
|
7022
7066
|
} = options ?? {};
|
|
7023
7067
|
const body = { ...extra, content };
|
|
@@ -7029,6 +7073,11 @@ var VideoClient = class {
|
|
|
7029
7073
|
if (seed !== void 0) body.seed = seed;
|
|
7030
7074
|
if (watermark !== void 0) body.watermark = watermark;
|
|
7031
7075
|
if (returnLastFrame !== void 0) body.return_last_frame = returnLastFrame;
|
|
7076
|
+
if (bitrateMode !== void 0) body.bitrate_mode = bitrateMode;
|
|
7077
|
+
if (outputFormat !== void 0) body.output_format = outputFormat;
|
|
7078
|
+
if (cameraFixed !== void 0) body.camera_fixed = cameraFixed;
|
|
7079
|
+
if (safetyIdentifier !== void 0) body.safety_identifier = safetyIdentifier;
|
|
7080
|
+
if (inputType !== void 0) body.input_type = inputType;
|
|
7032
7081
|
return this.submitAndPoll(body, budgetMs ?? DEFAULT_GENERATE_BUDGET_MS, "/v1/videos");
|
|
7033
7082
|
}
|
|
7034
7083
|
// --------------------------------------------------------------------
|
|
@@ -7565,8 +7614,8 @@ var PhoneClient = class {
|
|
|
7565
7614
|
return data;
|
|
7566
7615
|
}
|
|
7567
7616
|
// ─── Internals ─────────────────────────────────────────────────────────
|
|
7568
|
-
async request(
|
|
7569
|
-
const url = `${this.apiUrl}/v1/phone/${
|
|
7617
|
+
async request(path7, body) {
|
|
7618
|
+
const url = `${this.apiUrl}/v1/phone/${path7}`;
|
|
7570
7619
|
const response = await this.fetchWithTimeout(url, {
|
|
7571
7620
|
method: "POST",
|
|
7572
7621
|
headers: { "Content-Type": "application/json" },
|
|
@@ -8007,10 +8056,10 @@ var PriceClient = class {
|
|
|
8007
8056
|
}
|
|
8008
8057
|
async price(category, symbol, options) {
|
|
8009
8058
|
if (!symbol) throw new Error("symbol is required");
|
|
8010
|
-
const
|
|
8059
|
+
const path7 = categoryPath(category, options?.market, "price", symbol);
|
|
8011
8060
|
const query = {};
|
|
8012
8061
|
if (options?.session) query.session = options.session;
|
|
8013
|
-
const data = await this.getWithPayment(
|
|
8062
|
+
const data = await this.getWithPayment(path7, query);
|
|
8014
8063
|
return {
|
|
8015
8064
|
symbol: data.symbol ?? symbol.toUpperCase(),
|
|
8016
8065
|
price: data.price,
|
|
@@ -8029,14 +8078,14 @@ var PriceClient = class {
|
|
|
8029
8078
|
if (!options.from || options.from <= 0) {
|
|
8030
8079
|
throw new Error("history requires options.from (unix seconds)");
|
|
8031
8080
|
}
|
|
8032
|
-
const
|
|
8081
|
+
const path7 = categoryPath(category, options.market, "history", symbol);
|
|
8033
8082
|
const query = {
|
|
8034
8083
|
resolution: options.resolution ?? "D",
|
|
8035
8084
|
from: String(options.from)
|
|
8036
8085
|
};
|
|
8037
8086
|
if (options.to) query.to = String(options.to);
|
|
8038
8087
|
if (options.session) query.session = options.session;
|
|
8039
|
-
const data = await this.getWithPayment(
|
|
8088
|
+
const data = await this.getWithPayment(path7, query);
|
|
8040
8089
|
return {
|
|
8041
8090
|
symbol: data.symbol ?? symbol.toUpperCase(),
|
|
8042
8091
|
resolution: data.resolution ?? (options.resolution ?? "D"),
|
|
@@ -8048,12 +8097,12 @@ var PriceClient = class {
|
|
|
8048
8097
|
};
|
|
8049
8098
|
}
|
|
8050
8099
|
async listSymbols(category, options) {
|
|
8051
|
-
const
|
|
8100
|
+
const path7 = categoryPath(category, options?.market, "list");
|
|
8052
8101
|
const query = {
|
|
8053
8102
|
limit: String(options?.limit && options.limit > 0 ? options.limit : 100)
|
|
8054
8103
|
};
|
|
8055
8104
|
if (options?.query) query.q = options.query;
|
|
8056
|
-
const data = await this.getWithPayment(
|
|
8105
|
+
const data = await this.getWithPayment(path7, query);
|
|
8057
8106
|
if (Array.isArray(data)) {
|
|
8058
8107
|
return { symbols: data, count: data.length };
|
|
8059
8108
|
}
|
|
@@ -8226,19 +8275,19 @@ var SurfClient = class {
|
|
|
8226
8275
|
* `/v1/surf` is tolerated and stripped). Query params are URL-encoded;
|
|
8227
8276
|
* arrays become repeated keys (`?a=1&a=2`).
|
|
8228
8277
|
*/
|
|
8229
|
-
async get(
|
|
8230
|
-
const url = this.buildUrl(
|
|
8278
|
+
async get(path7, params) {
|
|
8279
|
+
const url = this.buildUrl(path7, params);
|
|
8231
8280
|
return this.requestWithPayment(url, "GET");
|
|
8232
8281
|
}
|
|
8233
8282
|
/**
|
|
8234
8283
|
* POST a Surf endpoint with a JSON body. Same path normalization as `get`.
|
|
8235
8284
|
*/
|
|
8236
|
-
async post(
|
|
8237
|
-
const url = this.buildUrl(
|
|
8285
|
+
async post(path7, body) {
|
|
8286
|
+
const url = this.buildUrl(path7);
|
|
8238
8287
|
return this.requestWithPayment(url, "POST", body);
|
|
8239
8288
|
}
|
|
8240
|
-
buildUrl(
|
|
8241
|
-
let normalized =
|
|
8289
|
+
buildUrl(path7, params) {
|
|
8290
|
+
let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
|
|
8242
8291
|
if (!normalized.startsWith(SURF_PREFIX)) {
|
|
8243
8292
|
normalized = `${SURF_PREFIX}${normalized}`;
|
|
8244
8293
|
}
|
|
@@ -8696,15 +8745,15 @@ var BlockrunClient = class {
|
|
|
8696
8745
|
* `/api` is tolerated and stripped). Query params are URL-encoded; arrays
|
|
8697
8746
|
* become repeated keys (`?a=1&a=2`); undefined/null are dropped.
|
|
8698
8747
|
*/
|
|
8699
|
-
async get(
|
|
8700
|
-
const url = this.buildUrl(
|
|
8748
|
+
async get(path7, params) {
|
|
8749
|
+
const url = this.buildUrl(path7, params);
|
|
8701
8750
|
return this.requestWithPayment(url, "GET");
|
|
8702
8751
|
}
|
|
8703
8752
|
/**
|
|
8704
8753
|
* POST a BlockRun endpoint with a JSON body.
|
|
8705
8754
|
*/
|
|
8706
|
-
async post(
|
|
8707
|
-
const url = this.buildUrl(
|
|
8755
|
+
async post(path7, body) {
|
|
8756
|
+
const url = this.buildUrl(path7);
|
|
8708
8757
|
return this.requestWithPayment(url, "POST", body);
|
|
8709
8758
|
}
|
|
8710
8759
|
/**
|
|
@@ -8728,8 +8777,8 @@ var BlockrunClient = class {
|
|
|
8728
8777
|
* short-circuits and returns the body. Most long-running endpoints (image,
|
|
8729
8778
|
* video, music, voice) return 202 with a poll_url.
|
|
8730
8779
|
*/
|
|
8731
|
-
async poll(
|
|
8732
|
-
const submitUrl = this.buildUrl(
|
|
8780
|
+
async poll(path7, body, options) {
|
|
8781
|
+
const submitUrl = this.buildUrl(path7);
|
|
8733
8782
|
const budgetMs = options?.budgetMs ?? DEFAULT_POLL_BUDGET_MS;
|
|
8734
8783
|
const intervalMs = options?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
8735
8784
|
const resp402 = await this.fetchWithTimeout(submitUrl, {
|
|
@@ -8829,8 +8878,8 @@ var BlockrunClient = class {
|
|
|
8829
8878
|
* process.stdout.write(chunk.choices?.[0]?.delta?.content ?? "");
|
|
8830
8879
|
* }
|
|
8831
8880
|
*/
|
|
8832
|
-
async *stream(
|
|
8833
|
-
const url = this.buildUrl(
|
|
8881
|
+
async *stream(path7, body) {
|
|
8882
|
+
const url = this.buildUrl(path7);
|
|
8834
8883
|
const requestBody = JSON.stringify(body ?? {});
|
|
8835
8884
|
const resp402 = await this.fetchWithTimeout(url, {
|
|
8836
8885
|
method: "POST",
|
|
@@ -8873,8 +8922,8 @@ var BlockrunClient = class {
|
|
|
8873
8922
|
// --------------------------------------------------------------------
|
|
8874
8923
|
// Internal: shared infrastructure
|
|
8875
8924
|
// --------------------------------------------------------------------
|
|
8876
|
-
buildUrl(
|
|
8877
|
-
let normalized =
|
|
8925
|
+
buildUrl(path7, params) {
|
|
8926
|
+
let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
|
|
8878
8927
|
if (normalized.startsWith("/api/")) {
|
|
8879
8928
|
normalized = normalized.slice(4);
|
|
8880
8929
|
}
|
|
@@ -9520,6 +9569,332 @@ async function getOrCreateSolanaWallet() {
|
|
|
9520
9569
|
return { address, privateKey, isNew: true };
|
|
9521
9570
|
}
|
|
9522
9571
|
|
|
9572
|
+
// src/solana-batch.ts
|
|
9573
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
9574
|
+
var path5 = __toESM(require("path"), 1);
|
|
9575
|
+
var BATCH_SCHEME = "batch-settlement";
|
|
9576
|
+
var INSTALL_HINT2 = "npm install @x402/core@~2.28.0 @x402/svm@~2.28.0 @solana/kit";
|
|
9577
|
+
async function load(pkg, importer) {
|
|
9578
|
+
try {
|
|
9579
|
+
return await importer();
|
|
9580
|
+
} catch (err) {
|
|
9581
|
+
throw new Error(
|
|
9582
|
+
`@blockrun/llm: Solana batch-settlement requires the optional peer dependency "${pkg}", which is not installed.
|
|
9583
|
+
|
|
9584
|
+
${INSTALL_HINT2}
|
|
9585
|
+
|
|
9586
|
+
Original error: ${err instanceof Error ? err.message : String(err)}`,
|
|
9587
|
+
{ cause: err }
|
|
9588
|
+
);
|
|
9589
|
+
}
|
|
9590
|
+
}
|
|
9591
|
+
var FileChannelStorage = class {
|
|
9592
|
+
constructor(file) {
|
|
9593
|
+
this.file = file;
|
|
9594
|
+
}
|
|
9595
|
+
file;
|
|
9596
|
+
read() {
|
|
9597
|
+
try {
|
|
9598
|
+
const parsed = JSON.parse(fs6.readFileSync(this.file, "utf8"));
|
|
9599
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
9600
|
+
} catch (err) {
|
|
9601
|
+
if (err.code === "ENOENT") return {};
|
|
9602
|
+
throw err;
|
|
9603
|
+
}
|
|
9604
|
+
}
|
|
9605
|
+
write(all) {
|
|
9606
|
+
fs6.mkdirSync(path5.dirname(this.file), { recursive: true, mode: 448 });
|
|
9607
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
9608
|
+
fs6.writeFileSync(tmp, JSON.stringify(all, null, 2), { mode: 384 });
|
|
9609
|
+
fs6.renameSync(tmp, this.file);
|
|
9610
|
+
}
|
|
9611
|
+
async get(key) {
|
|
9612
|
+
return this.read()[key];
|
|
9613
|
+
}
|
|
9614
|
+
async set(key, record) {
|
|
9615
|
+
const all = this.read();
|
|
9616
|
+
all[key] = record;
|
|
9617
|
+
this.write(all);
|
|
9618
|
+
}
|
|
9619
|
+
async delete(key) {
|
|
9620
|
+
const all = this.read();
|
|
9621
|
+
if (!(key in all)) return;
|
|
9622
|
+
delete all[key];
|
|
9623
|
+
this.write(all);
|
|
9624
|
+
}
|
|
9625
|
+
/** Forget every channel in this file. */
|
|
9626
|
+
clear() {
|
|
9627
|
+
try {
|
|
9628
|
+
fs6.unlinkSync(this.file);
|
|
9629
|
+
} catch (err) {
|
|
9630
|
+
if (err.code !== "ENOENT") throw err;
|
|
9631
|
+
}
|
|
9632
|
+
}
|
|
9633
|
+
};
|
|
9634
|
+
function processAlive(pid) {
|
|
9635
|
+
try {
|
|
9636
|
+
process.kill(pid, 0);
|
|
9637
|
+
return true;
|
|
9638
|
+
} catch (err) {
|
|
9639
|
+
return err.code === "EPERM";
|
|
9640
|
+
}
|
|
9641
|
+
}
|
|
9642
|
+
var heldLocks = /* @__PURE__ */ new Set();
|
|
9643
|
+
var exitHookInstalled = false;
|
|
9644
|
+
function lockChannelFile(file) {
|
|
9645
|
+
if (heldLocks.has(file)) return void 0;
|
|
9646
|
+
const lock = `${file}.lock`;
|
|
9647
|
+
fs6.mkdirSync(path5.dirname(lock), { recursive: true, mode: 448 });
|
|
9648
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
9649
|
+
try {
|
|
9650
|
+
fs6.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
9651
|
+
heldLocks.add(file);
|
|
9652
|
+
if (!exitHookInstalled) {
|
|
9653
|
+
exitHookInstalled = true;
|
|
9654
|
+
process.once("exit", () => {
|
|
9655
|
+
for (const held of heldLocks) {
|
|
9656
|
+
try {
|
|
9657
|
+
fs6.unlinkSync(`${held}.lock`);
|
|
9658
|
+
} catch {
|
|
9659
|
+
}
|
|
9660
|
+
}
|
|
9661
|
+
});
|
|
9662
|
+
}
|
|
9663
|
+
return void 0;
|
|
9664
|
+
} catch (err) {
|
|
9665
|
+
if (err.code !== "EEXIST") throw err;
|
|
9666
|
+
}
|
|
9667
|
+
const owner = Number(fs6.readFileSync(lock, "utf8").trim());
|
|
9668
|
+
if (Number.isInteger(owner) && owner > 0 && owner !== process.pid && processAlive(owner)) return owner;
|
|
9669
|
+
try {
|
|
9670
|
+
fs6.unlinkSync(lock);
|
|
9671
|
+
} catch {
|
|
9672
|
+
}
|
|
9673
|
+
}
|
|
9674
|
+
return -1;
|
|
9675
|
+
}
|
|
9676
|
+
function pendingDeposit(pending) {
|
|
9677
|
+
const entries = Array.isArray(pending) ? pending : [pending];
|
|
9678
|
+
return entries.some(
|
|
9679
|
+
(entry) => entry?.payment?.payload?.type === "deposit"
|
|
9680
|
+
);
|
|
9681
|
+
}
|
|
9682
|
+
function dropOrphanedPending(storage) {
|
|
9683
|
+
return {
|
|
9684
|
+
get: async (key) => {
|
|
9685
|
+
const record = await storage.get(key);
|
|
9686
|
+
if (!record?.pending || record.channelConfig?.voucherSigner !== "server") return record;
|
|
9687
|
+
if (!record.hasConfirmedState || pendingDeposit(record.pending)) {
|
|
9688
|
+
await storage.delete(key);
|
|
9689
|
+
return void 0;
|
|
9690
|
+
}
|
|
9691
|
+
const { pending: _pending, hasConfirmedState: _confirmed, ...confirmed } = record;
|
|
9692
|
+
await storage.set(key, confirmed);
|
|
9693
|
+
return confirmed;
|
|
9694
|
+
},
|
|
9695
|
+
set: (key, record) => storage.set(key, record),
|
|
9696
|
+
delete: (key) => storage.delete(key)
|
|
9697
|
+
};
|
|
9698
|
+
}
|
|
9699
|
+
function offersBatch(paymentRequired) {
|
|
9700
|
+
return (paymentRequired.accepts ?? []).some((accept) => accept.scheme === BATCH_SCHEME);
|
|
9701
|
+
}
|
|
9702
|
+
function atomicToUsd(amount) {
|
|
9703
|
+
return amount !== void 0 && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
|
|
9704
|
+
}
|
|
9705
|
+
async function isBatchRefusal(response) {
|
|
9706
|
+
if (response.status === 402) return "payment_required";
|
|
9707
|
+
if (![400, 403, 409, 503].includes(response.status)) return void 0;
|
|
9708
|
+
let body;
|
|
9709
|
+
try {
|
|
9710
|
+
body = await response.clone().json();
|
|
9711
|
+
} catch {
|
|
9712
|
+
return void 0;
|
|
9713
|
+
}
|
|
9714
|
+
const { error, code } = body ?? {};
|
|
9715
|
+
if (code === "PAYMENT_VERIFICATION_UNAVAILABLE") return code;
|
|
9716
|
+
return typeof error === "string" && error.startsWith("batch_") ? error : void 0;
|
|
9717
|
+
}
|
|
9718
|
+
var wallets = /* @__PURE__ */ new Map();
|
|
9719
|
+
var SolanaBatchPayer = class {
|
|
9720
|
+
constructor(init) {
|
|
9721
|
+
this.init = init;
|
|
9722
|
+
const { operators } = init.options;
|
|
9723
|
+
if (!Array.isArray(operators) || operators.length === 0 || operators.some((o) => typeof o !== "string" || !o)) {
|
|
9724
|
+
throw new Error("batch.operators must list at least one base58 operator public key");
|
|
9725
|
+
}
|
|
9726
|
+
}
|
|
9727
|
+
init;
|
|
9728
|
+
state;
|
|
9729
|
+
async storeFile() {
|
|
9730
|
+
const { channelStore } = this.init.options;
|
|
9731
|
+
if (channelStore === false) return void 0;
|
|
9732
|
+
return path5.resolve(channelStore ?? path5.join(paths().dir, "solana-batch", `${await this.init.address()}.json`));
|
|
9733
|
+
}
|
|
9734
|
+
/**
|
|
9735
|
+
* This wallet's shared batch state, or why this client cannot use batch:
|
|
9736
|
+
* another client in this process holds the wallet with a different trust
|
|
9737
|
+
* configuration, or another live process owns its channel file.
|
|
9738
|
+
*/
|
|
9739
|
+
wallet() {
|
|
9740
|
+
this.state ??= (async () => {
|
|
9741
|
+
const address = await this.init.address();
|
|
9742
|
+
const file = await this.storeFile();
|
|
9743
|
+
const { operators, maxDeposit } = this.init.options;
|
|
9744
|
+
const config = JSON.stringify([[...operators].sort(), String(maxDeposit ?? ""), file ?? null, this.init.rpcUrl]);
|
|
9745
|
+
const existing = wallets.get(address);
|
|
9746
|
+
if (existing) {
|
|
9747
|
+
return existing.config === config ? existing : "another client in this process uses this wallet with different batch options";
|
|
9748
|
+
}
|
|
9749
|
+
if (file) {
|
|
9750
|
+
const owner = lockChannelFile(file);
|
|
9751
|
+
if (owner !== void 0) return `channel store ${file} is in use by process ${owner}`;
|
|
9752
|
+
}
|
|
9753
|
+
const created = { config, busy: false, warned: /* @__PURE__ */ new Set() };
|
|
9754
|
+
wallets.set(address, created);
|
|
9755
|
+
return created;
|
|
9756
|
+
})();
|
|
9757
|
+
return this.state;
|
|
9758
|
+
}
|
|
9759
|
+
build(wallet) {
|
|
9760
|
+
wallet.client ??= (async () => {
|
|
9761
|
+
const [core, svm, kit] = await Promise.all([
|
|
9762
|
+
load("@x402/core", () => import("@x402/core/client")),
|
|
9763
|
+
load("@x402/svm", () => import("@x402/svm/batch-settlement/client")),
|
|
9764
|
+
load("@solana/kit", () => import("@solana/kit"))
|
|
9765
|
+
]);
|
|
9766
|
+
const signer = await kit.createKeyPairSignerFromBytes(await this.init.secretKey());
|
|
9767
|
+
const file = await this.storeFile();
|
|
9768
|
+
const storage = file ? dropOrphanedPending(new FileChannelStorage(file)) : void 0;
|
|
9769
|
+
const scheme = new svm.BatchSvmScheme(signer, {
|
|
9770
|
+
rpcUrl: this.init.rpcUrl,
|
|
9771
|
+
serverSignedChannelsPolicy: {
|
|
9772
|
+
allowedOperators: this.init.options.operators,
|
|
9773
|
+
maxDeposit: this.init.options.maxDeposit ?? svm.DEFAULT_SERVER_SIGNED_MAX_DEPOSIT
|
|
9774
|
+
},
|
|
9775
|
+
...storage ? { channelStorage: storage } : {}
|
|
9776
|
+
});
|
|
9777
|
+
const client = new core.x402Client().register("solana:*", scheme).registerPolicy(scheme.paymentPolicy);
|
|
9778
|
+
client.setSpendControls({ maxAmountPerPayment: false });
|
|
9779
|
+
return {
|
|
9780
|
+
http: new core.x402HTTPClient(client),
|
|
9781
|
+
refund: (url) => scheme.refund(url)
|
|
9782
|
+
};
|
|
9783
|
+
})();
|
|
9784
|
+
wallet.client.catch(() => {
|
|
9785
|
+
wallet.client = void 0;
|
|
9786
|
+
});
|
|
9787
|
+
return wallet.client;
|
|
9788
|
+
}
|
|
9789
|
+
/**
|
|
9790
|
+
* Drop everything this wallet knows about its channel, on disk and in
|
|
9791
|
+
* memory. The next call rebuilds the scheme, which reads the channel's real
|
|
9792
|
+
* state (deposit, open or closed) from the chain.
|
|
9793
|
+
*/
|
|
9794
|
+
async forget(wallet) {
|
|
9795
|
+
wallet.client = void 0;
|
|
9796
|
+
const file = await this.storeFile();
|
|
9797
|
+
if (file) new FileChannelStorage(file).clear();
|
|
9798
|
+
}
|
|
9799
|
+
warn(wallet, reason) {
|
|
9800
|
+
if (wallet?.warned.has(reason)) return;
|
|
9801
|
+
wallet?.warned.add(reason);
|
|
9802
|
+
console.error(`[@blockrun/llm] batch-settlement unavailable (${reason}); paying with exact`);
|
|
9803
|
+
}
|
|
9804
|
+
/**
|
|
9805
|
+
* Pay one 402 with batch-settlement, or say why the caller should use exact.
|
|
9806
|
+
*
|
|
9807
|
+
* `send` replays the original request with the payment headers. The
|
|
9808
|
+
* returned response is 2xx; any other answer either becomes a fallback (the
|
|
9809
|
+
* gateway charged nothing) or is returned to the caller's own error path.
|
|
9810
|
+
*/
|
|
9811
|
+
async pay(paymentRequired, send) {
|
|
9812
|
+
if (!offersBatch(paymentRequired)) return { kind: "fallback", reason: "not_offered" };
|
|
9813
|
+
const wallet = await this.wallet();
|
|
9814
|
+
if (typeof wallet === "string") {
|
|
9815
|
+
this.warn(void 0, wallet);
|
|
9816
|
+
return { kind: "fallback", reason: wallet };
|
|
9817
|
+
}
|
|
9818
|
+
if (wallet.busy) return { kind: "fallback", reason: "channel_busy" };
|
|
9819
|
+
wallet.busy = true;
|
|
9820
|
+
try {
|
|
9821
|
+
let http;
|
|
9822
|
+
let payload;
|
|
9823
|
+
try {
|
|
9824
|
+
http = (await this.build(wallet)).http;
|
|
9825
|
+
payload = await http.createPaymentPayload(paymentRequired);
|
|
9826
|
+
} catch (err) {
|
|
9827
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
9828
|
+
this.warn(wallet, reason);
|
|
9829
|
+
return { kind: "fallback", reason };
|
|
9830
|
+
}
|
|
9831
|
+
const deposit = payload.payload?.type === "deposit";
|
|
9832
|
+
let response;
|
|
9833
|
+
try {
|
|
9834
|
+
response = await send(http.encodePaymentSignatureHeader(payload));
|
|
9835
|
+
} catch (err) {
|
|
9836
|
+
if (deposit) await this.forget(wallet);
|
|
9837
|
+
else await this.settle(http, payload, () => null, 0);
|
|
9838
|
+
throw err;
|
|
9839
|
+
}
|
|
9840
|
+
const getHeader = (name) => response.headers.get(name);
|
|
9841
|
+
const settled = await this.settle(http, payload, getHeader, response.status);
|
|
9842
|
+
if (deposit && !(response.ok && settled?.success === true)) await this.forget(wallet);
|
|
9843
|
+
if (response.ok) {
|
|
9844
|
+
const charged = atomicToUsd(settled?.amount) ?? atomicToUsd(payload.accepted?.amount) ?? 0;
|
|
9845
|
+
return { kind: "paid", response, chargedUsd: charged };
|
|
9846
|
+
}
|
|
9847
|
+
const refusal = await isBatchRefusal(response);
|
|
9848
|
+
if (refusal) {
|
|
9849
|
+
this.warn(wallet, refusal);
|
|
9850
|
+
return { kind: "fallback", reason: refusal };
|
|
9851
|
+
}
|
|
9852
|
+
return { kind: "failed", response };
|
|
9853
|
+
} finally {
|
|
9854
|
+
wallet.busy = false;
|
|
9855
|
+
}
|
|
9856
|
+
}
|
|
9857
|
+
/**
|
|
9858
|
+
* Hand the gateway's answer to the scheme, which verifies the operator's
|
|
9859
|
+
* voucher and advances (or rolls back) the channel's local state.
|
|
9860
|
+
*
|
|
9861
|
+
* A malformed receipt must not lose a response the caller has been served,
|
|
9862
|
+
* so a scheme error is logged rather than thrown.
|
|
9863
|
+
*/
|
|
9864
|
+
async settle(http, payload, getHeader, status2) {
|
|
9865
|
+
try {
|
|
9866
|
+
return (await http.processPaymentResult(payload, getHeader, status2)).settleResponse;
|
|
9867
|
+
} catch (err) {
|
|
9868
|
+
console.error(
|
|
9869
|
+
`[@blockrun/llm] batch-settlement receipt not reconciled: ${err instanceof Error ? err.message : String(err)}`
|
|
9870
|
+
);
|
|
9871
|
+
return void 0;
|
|
9872
|
+
}
|
|
9873
|
+
}
|
|
9874
|
+
/**
|
|
9875
|
+
* Close the channel and return its unused escrow to the wallet.
|
|
9876
|
+
*
|
|
9877
|
+
* The scheme keeps a closed channel in its records, and would go on paying
|
|
9878
|
+
* into it, so a successful close forgets the channel. The next batch call
|
|
9879
|
+
* finds no open channel on-chain and opens a new one.
|
|
9880
|
+
*
|
|
9881
|
+
* @param url - any batch-enabled route on the gateway the channel was opened with.
|
|
9882
|
+
*/
|
|
9883
|
+
async close(url) {
|
|
9884
|
+
const wallet = await this.wallet();
|
|
9885
|
+
if (typeof wallet === "string") throw new Error(`batch-settlement unavailable: ${wallet}`);
|
|
9886
|
+
if (wallet.busy) throw new Error("batch-settlement channel has a request in flight; close it when the call returns");
|
|
9887
|
+
wallet.busy = true;
|
|
9888
|
+
try {
|
|
9889
|
+
const result = await (await this.build(wallet)).refund(url);
|
|
9890
|
+
await this.forget(wallet);
|
|
9891
|
+
return result;
|
|
9892
|
+
} finally {
|
|
9893
|
+
wallet.busy = false;
|
|
9894
|
+
}
|
|
9895
|
+
}
|
|
9896
|
+
};
|
|
9897
|
+
|
|
9523
9898
|
// src/solana-client.ts
|
|
9524
9899
|
var SOLANA_API_URL = "https://sol.blockrun.ai/api";
|
|
9525
9900
|
var DEFAULT_MAX_TOKENS2 = 1024;
|
|
@@ -9583,7 +9958,7 @@ async function isSafeStaleBlockhashResponse(response) {
|
|
|
9583
9958
|
}
|
|
9584
9959
|
async function waitForStaleRetry(attempt) {
|
|
9585
9960
|
await new Promise(
|
|
9586
|
-
(
|
|
9961
|
+
(resolve2) => setTimeout(resolve2, STALE_BLOCKHASH_RETRY_BACKOFFS_MS[attempt])
|
|
9587
9962
|
);
|
|
9588
9963
|
}
|
|
9589
9964
|
var DEFAULT_SOLANA_RPC_URL = "https://sol.blockrun.ai/api/v1/solana/rpc";
|
|
@@ -9627,6 +10002,7 @@ var SolanaLLMClient = class {
|
|
|
9627
10002
|
addressCache = null;
|
|
9628
10003
|
modelPricingCache = null;
|
|
9629
10004
|
modelPricingPromise = null;
|
|
10005
|
+
batchPayer;
|
|
9630
10006
|
constructor(options = {}) {
|
|
9631
10007
|
this.apiAuth = resolveApiKeyAuth(options);
|
|
9632
10008
|
if (!this.apiAuth) {
|
|
@@ -9646,6 +10022,25 @@ var SolanaLLMClient = class {
|
|
|
9646
10022
|
this.rpcUrl = rpc.url;
|
|
9647
10023
|
this.rpcHeaders = rpc.headers;
|
|
9648
10024
|
this.timeout = options.timeout || DEFAULT_TIMEOUT14;
|
|
10025
|
+
if (options.batch && !this.apiAuth) {
|
|
10026
|
+
this.batchPayer = new SolanaBatchPayer({
|
|
10027
|
+
options: options.batch,
|
|
10028
|
+
secretKey: () => solanaKeyToBytes(this.privateKey),
|
|
10029
|
+
address: () => this.getWalletAddress(),
|
|
10030
|
+
rpcUrl: this.rpcUrl
|
|
10031
|
+
});
|
|
10032
|
+
}
|
|
10033
|
+
}
|
|
10034
|
+
/**
|
|
10035
|
+
* Close this wallet's batch-settlement channel and return its unused escrow.
|
|
10036
|
+
*
|
|
10037
|
+
* The gateway closes it cooperatively when it can; otherwise this starts a
|
|
10038
|
+
* payer-forced close and the escrow comes back after the channel's grace
|
|
10039
|
+
* period. Only meaningful with the `batch` option.
|
|
10040
|
+
*/
|
|
10041
|
+
async closeBatchChannel() {
|
|
10042
|
+
if (!this.batchPayer) throw new Error("closeBatchChannel() requires the `batch` option in wallet mode");
|
|
10043
|
+
return this.batchPayer.close(`${this.apiUrl}/v1/chat/completions`);
|
|
9649
10044
|
}
|
|
9650
10045
|
/** Get Solana wallet address (public key in base58). */
|
|
9651
10046
|
async getWalletAddress() {
|
|
@@ -9839,8 +10234,8 @@ var SolanaLLMClient = class {
|
|
|
9839
10234
|
* how you end up debugging a request you did not send.
|
|
9840
10235
|
* @returns each decoded SSE frame, in order.
|
|
9841
10236
|
*/
|
|
9842
|
-
async *stream(
|
|
9843
|
-
const url = this.buildUrl(
|
|
10237
|
+
async *stream(path7, body) {
|
|
10238
|
+
const url = this.buildUrl(path7);
|
|
9844
10239
|
const response = await this.openPaidStream(url, JSON.stringify(body ?? {}));
|
|
9845
10240
|
yield* readSseFrames(
|
|
9846
10241
|
response,
|
|
@@ -9912,8 +10307,8 @@ var SolanaLLMClient = class {
|
|
|
9912
10307
|
* @param path - endpoint path, with or without a leading slash.
|
|
9913
10308
|
* @returns the absolute URL to call.
|
|
9914
10309
|
*/
|
|
9915
|
-
buildUrl(
|
|
9916
|
-
let normalized =
|
|
10310
|
+
buildUrl(path7) {
|
|
10311
|
+
let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
|
|
9917
10312
|
if (normalized.startsWith("/api/")) normalized = normalized.slice(4);
|
|
9918
10313
|
return `${this.apiUrl}${normalized}`;
|
|
9919
10314
|
}
|
|
@@ -9992,11 +10387,11 @@ var SolanaLLMClient = class {
|
|
|
9992
10387
|
return data;
|
|
9993
10388
|
}
|
|
9994
10389
|
// ── Prediction Markets (Powered by Predexon) ────────────────────────────
|
|
9995
|
-
async pm(
|
|
9996
|
-
return this.getWithPaymentRaw(`/v1/pm/${
|
|
10390
|
+
async pm(path7, params) {
|
|
10391
|
+
return this.getWithPaymentRaw(`/v1/pm/${path7}`, params);
|
|
9997
10392
|
}
|
|
9998
|
-
async pmQuery(
|
|
9999
|
-
return this.requestWithPaymentRaw(`/v1/pm/${
|
|
10393
|
+
async pmQuery(path7, query) {
|
|
10394
|
+
return this.requestWithPaymentRaw(`/v1/pm/${path7}`, query);
|
|
10000
10395
|
}
|
|
10001
10396
|
// ── Exa Web Search (Powered by Exa) ──────────────────────────────────────
|
|
10002
10397
|
/**
|
|
@@ -10008,8 +10403,8 @@ var SolanaLLMClient = class {
|
|
|
10008
10403
|
* @example
|
|
10009
10404
|
* const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
|
|
10010
10405
|
*/
|
|
10011
|
-
async exa(
|
|
10012
|
-
return this.requestWithPaymentRaw(`/v1/exa/${
|
|
10406
|
+
async exa(path7, body) {
|
|
10407
|
+
return this.requestWithPaymentRaw(`/v1/exa/${path7}`, body);
|
|
10013
10408
|
}
|
|
10014
10409
|
/**
|
|
10015
10410
|
* Neural and keyword web search via Exa (Solana payment).
|
|
@@ -10059,8 +10454,8 @@ var SolanaLLMClient = class {
|
|
|
10059
10454
|
* "prices/coingecko:bitcoin,base:0x..."
|
|
10060
10455
|
* @param params - Query parameters passed through to DefiLlama
|
|
10061
10456
|
*/
|
|
10062
|
-
async defi(
|
|
10063
|
-
return this.getWithPaymentRaw(`/v1/defillama/${
|
|
10457
|
+
async defi(path7, params) {
|
|
10458
|
+
return this.getWithPaymentRaw(`/v1/defillama/${path7}`, params);
|
|
10064
10459
|
}
|
|
10065
10460
|
/** All DeFi protocols with TVL. */
|
|
10066
10461
|
async defiProtocols() {
|
|
@@ -10096,9 +10491,9 @@ var SolanaLLMClient = class {
|
|
|
10096
10491
|
* @param params - Query params (chainId, sellToken, buyToken, sellAmount, taker, ...)
|
|
10097
10492
|
* @param body - JSON body — pass to switch to POST (gasless/submit only)
|
|
10098
10493
|
*/
|
|
10099
|
-
async dex(
|
|
10100
|
-
if (body) return this.requestWithPaymentRaw(`/v1/zerox/${
|
|
10101
|
-
return this.getWithPaymentRaw(`/v1/zerox/${
|
|
10494
|
+
async dex(path7, params, body) {
|
|
10495
|
+
if (body) return this.requestWithPaymentRaw(`/v1/zerox/${path7}`, body);
|
|
10496
|
+
return this.getWithPaymentRaw(`/v1/zerox/${path7}`, params);
|
|
10102
10497
|
}
|
|
10103
10498
|
/** Indicative Permit2 swap price — no commitment (free). */
|
|
10104
10499
|
async dexPrice(params) {
|
|
@@ -10142,8 +10537,8 @@ var SolanaLLMClient = class {
|
|
|
10142
10537
|
* "sandbox/status", "sandbox/terminate"
|
|
10143
10538
|
* @param body - JSON body for the endpoint
|
|
10144
10539
|
*/
|
|
10145
|
-
async modal(
|
|
10146
|
-
return this.requestWithPaymentRaw(`/v1/modal/${
|
|
10540
|
+
async modal(path7, body) {
|
|
10541
|
+
return this.requestWithPaymentRaw(`/v1/modal/${path7}`, body ?? {});
|
|
10147
10542
|
}
|
|
10148
10543
|
/** Create a sandboxed compute environment (CPU / GPU tiers). */
|
|
10149
10544
|
async modalSandboxCreate(body) {
|
|
@@ -10180,7 +10575,31 @@ var SolanaLLMClient = class {
|
|
|
10180
10575
|
});
|
|
10181
10576
|
if (response.status === 402) {
|
|
10182
10577
|
try {
|
|
10183
|
-
|
|
10578
|
+
const paymentRequired = await this.readPaymentRequired(response);
|
|
10579
|
+
if (this.batchPayer && staleRetries === 0) {
|
|
10580
|
+
const batch = await this.batchPayer.pay(
|
|
10581
|
+
paymentRequired,
|
|
10582
|
+
(paymentHeaders) => this.fetchWithTimeout(url, {
|
|
10583
|
+
method: "POST",
|
|
10584
|
+
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...paymentHeaders },
|
|
10585
|
+
body: JSON.stringify(body)
|
|
10586
|
+
})
|
|
10587
|
+
);
|
|
10588
|
+
if (batch.kind === "paid") {
|
|
10589
|
+
this.recordSettlement(batch.chargedUsd);
|
|
10590
|
+
return batch.response.json();
|
|
10591
|
+
}
|
|
10592
|
+
if (batch.kind === "failed") {
|
|
10593
|
+
let errorBody;
|
|
10594
|
+
try {
|
|
10595
|
+
errorBody = await batch.response.json();
|
|
10596
|
+
} catch {
|
|
10597
|
+
errorBody = { error: "Request failed" };
|
|
10598
|
+
}
|
|
10599
|
+
throw new APIError(`API error after payment: ${batch.response.status}`, batch.response.status, sanitizeErrorResponse(errorBody));
|
|
10600
|
+
}
|
|
10601
|
+
}
|
|
10602
|
+
return await this.handlePaymentAndRetry(url, body, paymentRequired, staleRetries > 0);
|
|
10184
10603
|
} catch (error) {
|
|
10185
10604
|
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
10186
10605
|
await waitForStaleRetry(staleRetries++);
|
|
@@ -10218,7 +10637,7 @@ var SolanaLLMClient = class {
|
|
|
10218
10637
|
* @throws PaymentError when the 402 carries no usable Solana requirements.
|
|
10219
10638
|
*/
|
|
10220
10639
|
/**
|
|
10221
|
-
* Follow a 202 `{ id, poll_url }` to completion, on Solana.
|
|
10640
|
+
* Follow a 202 `{ id, poll_url }` to completion, on Solana **image** routes.
|
|
10222
10641
|
*
|
|
10223
10642
|
* Two things differ from Base and both come from the same constraint — a
|
|
10224
10643
|
* Solana payment is a transaction pinned to a recent blockhash, valid for
|
|
@@ -10229,6 +10648,9 @@ var SolanaLLMClient = class {
|
|
|
10229
10648
|
* settles optimistically at submit, and this loop only fetches the result.
|
|
10230
10649
|
* The cost is recorded by the caller at POST, never here — recording it on
|
|
10231
10650
|
* completion would lose the charge whenever a paid job then fails.
|
|
10651
|
+
* Solana VIDEO is the exception: it settles on the completed poll and a
|
|
10652
|
+
* failed job is not charged. Do not route video through this loop as-is —
|
|
10653
|
+
* it would book spend for jobs that were never charged.
|
|
10232
10654
|
* 2. **Every poll re-signs.** One authorization cannot be replayed across a
|
|
10233
10655
|
* long render. The gateway binds the job to the payer address rather than
|
|
10234
10656
|
* to the signature, so a fresh signature from the same wallet is accepted
|
|
@@ -10298,6 +10720,15 @@ var SolanaLLMClient = class {
|
|
|
10298
10720
|
);
|
|
10299
10721
|
}
|
|
10300
10722
|
async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
|
|
10723
|
+
return this.signExactPayment(
|
|
10724
|
+
url,
|
|
10725
|
+
await this.readPaymentRequired(response),
|
|
10726
|
+
forceFreshBlockhash,
|
|
10727
|
+
resourceFallback
|
|
10728
|
+
);
|
|
10729
|
+
}
|
|
10730
|
+
/** Read a `402`'s payment requirements from its header, or its JSON body. */
|
|
10731
|
+
async readPaymentRequired(response) {
|
|
10301
10732
|
let paymentHeader = response.headers.get("payment-required");
|
|
10302
10733
|
if (!paymentHeader) {
|
|
10303
10734
|
try {
|
|
@@ -10311,8 +10742,14 @@ var SolanaLLMClient = class {
|
|
|
10311
10742
|
if (!paymentHeader) {
|
|
10312
10743
|
throw new PaymentError("402 response but no payment requirements found");
|
|
10313
10744
|
}
|
|
10314
|
-
|
|
10315
|
-
|
|
10745
|
+
return parsePaymentRequired(paymentHeader);
|
|
10746
|
+
}
|
|
10747
|
+
/** Sign the `exact` accept of a 402 as an SPL TransferChecked payment. */
|
|
10748
|
+
async signExactPayment(url, paymentRequired, forceFreshBlockhash, resourceFallback = url) {
|
|
10749
|
+
const details = extractPaymentDetails(
|
|
10750
|
+
{ ...paymentRequired, accepts: paymentRequired.accepts.filter((accept) => accept.scheme !== BATCH_SCHEME) },
|
|
10751
|
+
SOLANA_NETWORK
|
|
10752
|
+
);
|
|
10316
10753
|
if (!details.network?.startsWith("solana:")) {
|
|
10317
10754
|
throw new PaymentError(
|
|
10318
10755
|
`Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
|
|
@@ -10345,10 +10782,10 @@ var SolanaLLMClient = class {
|
|
|
10345
10782
|
);
|
|
10346
10783
|
return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
|
|
10347
10784
|
}
|
|
10348
|
-
async handlePaymentAndRetry(url, body,
|
|
10349
|
-
const { paymentPayload, costUsd } = await this.
|
|
10785
|
+
async handlePaymentAndRetry(url, body, paymentRequired, forceFreshBlockhash = false) {
|
|
10786
|
+
const { paymentPayload, costUsd } = await this.signExactPayment(
|
|
10350
10787
|
url,
|
|
10351
|
-
|
|
10788
|
+
paymentRequired,
|
|
10352
10789
|
forceFreshBlockhash,
|
|
10353
10790
|
`${this.apiUrl}/v1/chat/completions`
|
|
10354
10791
|
);
|
|
@@ -10509,13 +10946,13 @@ function solanaClient(options = {}) {
|
|
|
10509
10946
|
}
|
|
10510
10947
|
|
|
10511
10948
|
// src/cache.ts
|
|
10512
|
-
var
|
|
10513
|
-
var
|
|
10949
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
10950
|
+
var path6 = __toESM(require("path"), 1);
|
|
10514
10951
|
var os4 = __toESM(require("os"), 1);
|
|
10515
10952
|
var crypto2 = __toESM(require("crypto"), 1);
|
|
10516
|
-
var CACHE_DIR =
|
|
10517
|
-
var DATA_DIR =
|
|
10518
|
-
var COST_LOG_FILE2 =
|
|
10953
|
+
var CACHE_DIR = path6.join(os4.homedir(), ".blockrun", "cache");
|
|
10954
|
+
var DATA_DIR = path6.join(os4.homedir(), ".blockrun", "data");
|
|
10955
|
+
var COST_LOG_FILE2 = path6.join(os4.homedir(), ".blockrun", "cost_log.jsonl");
|
|
10519
10956
|
var DEFAULT_TTL = {
|
|
10520
10957
|
"/v1/partner/": 3600 * 1e3,
|
|
10521
10958
|
"/v1/pm/": 1800 * 1e3,
|
|
@@ -10535,19 +10972,19 @@ function cacheKey(endpoint, body) {
|
|
|
10535
10972
|
return crypto2.createHash("sha256").update(keyData).digest("hex").slice(0, 16);
|
|
10536
10973
|
}
|
|
10537
10974
|
function cachePath(key) {
|
|
10538
|
-
return
|
|
10975
|
+
return path6.join(CACHE_DIR, `${key}.json`);
|
|
10539
10976
|
}
|
|
10540
10977
|
function getCached(key) {
|
|
10541
10978
|
const filePath = cachePath(key);
|
|
10542
|
-
if (!
|
|
10979
|
+
if (!fs7.existsSync(filePath)) return null;
|
|
10543
10980
|
try {
|
|
10544
|
-
const raw =
|
|
10981
|
+
const raw = fs7.readFileSync(filePath, "utf-8");
|
|
10545
10982
|
const entry = JSON.parse(raw);
|
|
10546
10983
|
const ttl = entry.ttlMs ?? getTtl(entry.endpoint ?? "");
|
|
10547
10984
|
if (ttl <= 0) return null;
|
|
10548
10985
|
if (Date.now() - entry.cachedAt > ttl) {
|
|
10549
10986
|
try {
|
|
10550
|
-
|
|
10987
|
+
fs7.unlinkSync(filePath);
|
|
10551
10988
|
} catch {
|
|
10552
10989
|
}
|
|
10553
10990
|
return null;
|
|
@@ -10566,7 +11003,7 @@ function getCachedByRequest(endpoint, body) {
|
|
|
10566
11003
|
function setCache(key, data, ttlMs) {
|
|
10567
11004
|
if (ttlMs <= 0) return;
|
|
10568
11005
|
try {
|
|
10569
|
-
|
|
11006
|
+
fs7.mkdirSync(CACHE_DIR, { recursive: true });
|
|
10570
11007
|
} catch {
|
|
10571
11008
|
}
|
|
10572
11009
|
const entry = {
|
|
@@ -10575,7 +11012,7 @@ function setCache(key, data, ttlMs) {
|
|
|
10575
11012
|
ttlMs
|
|
10576
11013
|
};
|
|
10577
11014
|
try {
|
|
10578
|
-
|
|
11015
|
+
fs7.writeFileSync(cachePath(key), JSON.stringify(entry));
|
|
10579
11016
|
} catch {
|
|
10580
11017
|
}
|
|
10581
11018
|
}
|
|
@@ -10596,7 +11033,7 @@ function readableFilename(endpoint, body) {
|
|
|
10596
11033
|
}
|
|
10597
11034
|
function saveReadable(endpoint, body, response, costUsd) {
|
|
10598
11035
|
try {
|
|
10599
|
-
|
|
11036
|
+
fs7.mkdirSync(DATA_DIR, { recursive: true });
|
|
10600
11037
|
} catch {
|
|
10601
11038
|
}
|
|
10602
11039
|
const filename = readableFilename(endpoint, body);
|
|
@@ -10608,14 +11045,14 @@ function saveReadable(endpoint, body, response, costUsd) {
|
|
|
10608
11045
|
response
|
|
10609
11046
|
};
|
|
10610
11047
|
try {
|
|
10611
|
-
|
|
11048
|
+
fs7.writeFileSync(path6.join(DATA_DIR, filename), JSON.stringify(entry, null, 2));
|
|
10612
11049
|
} catch {
|
|
10613
11050
|
}
|
|
10614
11051
|
}
|
|
10615
11052
|
function appendCostLog(endpoint, costUsd) {
|
|
10616
11053
|
if (costUsd <= 0) return;
|
|
10617
11054
|
try {
|
|
10618
|
-
|
|
11055
|
+
fs7.mkdirSync(path6.dirname(COST_LOG_FILE2), { recursive: true });
|
|
10619
11056
|
} catch {
|
|
10620
11057
|
}
|
|
10621
11058
|
const entry = {
|
|
@@ -10624,7 +11061,7 @@ function appendCostLog(endpoint, costUsd) {
|
|
|
10624
11061
|
cost_usd: costUsd
|
|
10625
11062
|
};
|
|
10626
11063
|
try {
|
|
10627
|
-
|
|
11064
|
+
fs7.appendFileSync(COST_LOG_FILE2, JSON.stringify(entry) + "\n");
|
|
10628
11065
|
} catch {
|
|
10629
11066
|
}
|
|
10630
11067
|
}
|
|
@@ -10632,7 +11069,7 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
|
|
|
10632
11069
|
const ttl = getTtl(endpoint);
|
|
10633
11070
|
if (ttl > 0) {
|
|
10634
11071
|
try {
|
|
10635
|
-
|
|
11072
|
+
fs7.mkdirSync(CACHE_DIR, { recursive: true });
|
|
10636
11073
|
} catch {
|
|
10637
11074
|
}
|
|
10638
11075
|
const key = cacheKey(endpoint, body);
|
|
@@ -10644,7 +11081,7 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
|
|
|
10644
11081
|
costUsd
|
|
10645
11082
|
};
|
|
10646
11083
|
try {
|
|
10647
|
-
|
|
11084
|
+
fs7.writeFileSync(cachePath(key), JSON.stringify(entry));
|
|
10648
11085
|
} catch {
|
|
10649
11086
|
}
|
|
10650
11087
|
}
|
|
@@ -10652,14 +11089,14 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
|
|
|
10652
11089
|
appendCostLog(endpoint, costUsd);
|
|
10653
11090
|
}
|
|
10654
11091
|
function clearCache() {
|
|
10655
|
-
if (!
|
|
11092
|
+
if (!fs7.existsSync(CACHE_DIR)) return 0;
|
|
10656
11093
|
let count = 0;
|
|
10657
11094
|
try {
|
|
10658
|
-
const files =
|
|
11095
|
+
const files = fs7.readdirSync(CACHE_DIR);
|
|
10659
11096
|
for (const file of files) {
|
|
10660
11097
|
if (file.endsWith(".json")) {
|
|
10661
11098
|
try {
|
|
10662
|
-
|
|
11099
|
+
fs7.unlinkSync(path6.join(CACHE_DIR, file));
|
|
10663
11100
|
count++;
|
|
10664
11101
|
} catch {
|
|
10665
11102
|
}
|
|
@@ -10670,14 +11107,14 @@ function clearCache() {
|
|
|
10670
11107
|
return count;
|
|
10671
11108
|
}
|
|
10672
11109
|
function getCostLogSummary() {
|
|
10673
|
-
if (!
|
|
11110
|
+
if (!fs7.existsSync(COST_LOG_FILE2)) {
|
|
10674
11111
|
return { totalUsd: 0, calls: 0, byEndpoint: {} };
|
|
10675
11112
|
}
|
|
10676
11113
|
let totalUsd = 0;
|
|
10677
11114
|
let calls = 0;
|
|
10678
11115
|
const byEndpoint = {};
|
|
10679
11116
|
try {
|
|
10680
|
-
const content =
|
|
11117
|
+
const content = fs7.readFileSync(COST_LOG_FILE2, "utf-8").trim();
|
|
10681
11118
|
if (!content) return { totalUsd: 0, calls: 0, byEndpoint: {} };
|
|
10682
11119
|
for (const line of content.split("\n")) {
|
|
10683
11120
|
if (!line) continue;
|