@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.js
CHANGED
|
@@ -163,7 +163,7 @@ var TRANSIENT_RETRIES = 2;
|
|
|
163
163
|
var TRANSIENT_BACKOFF_MS = 1e3;
|
|
164
164
|
function retryDelay(ms, signal) {
|
|
165
165
|
signal?.throwIfAborted();
|
|
166
|
-
return new Promise((
|
|
166
|
+
return new Promise((resolve2, reject) => {
|
|
167
167
|
const onAbort = () => {
|
|
168
168
|
clearTimeout(timer);
|
|
169
169
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -171,7 +171,7 @@ function retryDelay(ms, signal) {
|
|
|
171
171
|
};
|
|
172
172
|
const timer = setTimeout(() => {
|
|
173
173
|
signal?.removeEventListener("abort", onAbort);
|
|
174
|
-
|
|
174
|
+
resolve2();
|
|
175
175
|
}, ms);
|
|
176
176
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
177
177
|
});
|
|
@@ -207,8 +207,8 @@ var ApiKeyAuth = class {
|
|
|
207
207
|
this.#key = key;
|
|
208
208
|
}
|
|
209
209
|
/** Resolve gateway poll paths without forwarding credentials to another origin. */
|
|
210
|
-
resolveUrl(
|
|
211
|
-
const url = new URL(
|
|
210
|
+
resolveUrl(path7) {
|
|
211
|
+
const url = new URL(path7, this.apiUrl + "/");
|
|
212
212
|
if (url.origin !== new URL(this.apiUrl).origin || url.username || url.password) {
|
|
213
213
|
throw new Error("Refusing to send a BlockRun API key to a different origin.");
|
|
214
214
|
}
|
|
@@ -274,7 +274,7 @@ var ApiKeyAuth = class {
|
|
|
274
274
|
if (["failed", "cancelled", "canceled"].includes(String(data.status))) {
|
|
275
275
|
throw new APIError("Account API job failed or was cancelled", 502, sanitizeErrorResponse(data));
|
|
276
276
|
}
|
|
277
|
-
await new Promise((
|
|
277
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.min(interval, Math.max(0, deadline - Date.now()))));
|
|
278
278
|
const remaining = deadline - Date.now();
|
|
279
279
|
if (remaining <= 0) break;
|
|
280
280
|
try {
|
|
@@ -4539,7 +4539,7 @@ function getCostSummary() {
|
|
|
4539
4539
|
}
|
|
4540
4540
|
|
|
4541
4541
|
// src/version.ts
|
|
4542
|
-
var SDK_VERSION = "3.
|
|
4542
|
+
var SDK_VERSION = "3.19.0";
|
|
4543
4543
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
4544
4544
|
|
|
4545
4545
|
// src/client.ts
|
|
@@ -5648,8 +5648,8 @@ var LLMClient = class _LLMClient {
|
|
|
5648
5648
|
* @example
|
|
5649
5649
|
* const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
|
|
5650
5650
|
*/
|
|
5651
|
-
async exa(
|
|
5652
|
-
return this.requestWithPaymentRaw(`/v1/exa/${
|
|
5651
|
+
async exa(path7, body) {
|
|
5652
|
+
return this.requestWithPaymentRaw(`/v1/exa/${path7}`, body);
|
|
5653
5653
|
}
|
|
5654
5654
|
/**
|
|
5655
5655
|
* Neural web search via Exa. Returns semantically relevant URLs and metadata.
|
|
@@ -5797,8 +5797,8 @@ var LLMClient = class _LLMClient {
|
|
|
5797
5797
|
* const market = await client.pm("kalshi/markets/KXBTC-25MAR14");
|
|
5798
5798
|
* const results = await client.pm("polymarket/search", { q: "bitcoin" });
|
|
5799
5799
|
*/
|
|
5800
|
-
async pm(
|
|
5801
|
-
return this.getWithPaymentRaw(`/v1/pm/${
|
|
5800
|
+
async pm(path7, params) {
|
|
5801
|
+
return this.getWithPaymentRaw(`/v1/pm/${path7}`, params);
|
|
5802
5802
|
}
|
|
5803
5803
|
/**
|
|
5804
5804
|
* Structured query for Predexon prediction market data (POST endpoints).
|
|
@@ -5814,8 +5814,8 @@ var LLMClient = class _LLMClient {
|
|
|
5814
5814
|
* addresses: ["0xabc...", "0xdef..."],
|
|
5815
5815
|
* });
|
|
5816
5816
|
*/
|
|
5817
|
-
async pmQuery(
|
|
5818
|
-
return this.requestWithPaymentRaw(`/v1/pm/${
|
|
5817
|
+
async pmQuery(path7, query) {
|
|
5818
|
+
return this.requestWithPaymentRaw(`/v1/pm/${path7}`, query);
|
|
5819
5819
|
}
|
|
5820
5820
|
// ── PM convenience helpers (Predexon v2) ─────────────────────────────────
|
|
5821
5821
|
// Thin wrappers over pm() / pmQuery() for the most common v2 endpoints.
|
|
@@ -5903,8 +5903,8 @@ var LLMClient = class _LLMClient {
|
|
|
5903
5903
|
* "prices/coingecko:bitcoin,base:0x..."
|
|
5904
5904
|
* @param params - Query parameters passed through to DefiLlama
|
|
5905
5905
|
*/
|
|
5906
|
-
async defi(
|
|
5907
|
-
return this.getWithPaymentRaw(`/v1/defillama/${
|
|
5906
|
+
async defi(path7, params) {
|
|
5907
|
+
return this.getWithPaymentRaw(`/v1/defillama/${path7}`, params);
|
|
5908
5908
|
}
|
|
5909
5909
|
/** All DeFi protocols with TVL. */
|
|
5910
5910
|
async defiProtocols() {
|
|
@@ -5940,9 +5940,9 @@ var LLMClient = class _LLMClient {
|
|
|
5940
5940
|
* @param params - Query params (chainId, sellToken, buyToken, sellAmount, taker, ...)
|
|
5941
5941
|
* @param body - JSON body — pass to switch to POST (gasless/submit only)
|
|
5942
5942
|
*/
|
|
5943
|
-
async dex(
|
|
5944
|
-
if (body) return this.requestWithPaymentRaw(`/v1/zerox/${
|
|
5945
|
-
return this.getWithPaymentRaw(`/v1/zerox/${
|
|
5943
|
+
async dex(path7, params, body) {
|
|
5944
|
+
if (body) return this.requestWithPaymentRaw(`/v1/zerox/${path7}`, body);
|
|
5945
|
+
return this.getWithPaymentRaw(`/v1/zerox/${path7}`, params);
|
|
5946
5946
|
}
|
|
5947
5947
|
/** Indicative Permit2 swap price — no commitment (free). */
|
|
5948
5948
|
async dexPrice(params) {
|
|
@@ -5986,8 +5986,8 @@ var LLMClient = class _LLMClient {
|
|
|
5986
5986
|
* "sandbox/status", "sandbox/terminate"
|
|
5987
5987
|
* @param body - JSON body for the endpoint
|
|
5988
5988
|
*/
|
|
5989
|
-
async modal(
|
|
5990
|
-
return this.requestWithPaymentRaw(`/v1/modal/${
|
|
5989
|
+
async modal(path7, body) {
|
|
5990
|
+
return this.requestWithPaymentRaw(`/v1/modal/${path7}`, body ?? {});
|
|
5991
5991
|
}
|
|
5992
5992
|
/** Create a sandboxed compute environment (CPU / GPU tiers). */
|
|
5993
5993
|
async modalSandboxCreate(body) {
|
|
@@ -6295,7 +6295,7 @@ var ImageClient = class {
|
|
|
6295
6295
|
const pollUrl = pollPath.startsWith("/api/") ? `${this.apiUrl.replace(/\/api$/, "")}${pollPath}` : `${this.apiUrl}${pollPath.startsWith("/") ? "" : "/"}${pollPath}`;
|
|
6296
6296
|
const startedAt = Date.now();
|
|
6297
6297
|
while (Date.now() - startedAt < POLL_MAX_DURATION_MS) {
|
|
6298
|
-
await new Promise((
|
|
6298
|
+
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
6299
6299
|
const pollResp = await this.fetchWithTimeout(pollUrl, {
|
|
6300
6300
|
method: "GET",
|
|
6301
6301
|
headers: {
|
|
@@ -6837,8 +6837,36 @@ var VideoClient = class {
|
|
|
6837
6837
|
"referenceImageUrls is mutually exclusive with imageUrl, lastFrameUrl, and realFaceAssetId."
|
|
6838
6838
|
);
|
|
6839
6839
|
}
|
|
6840
|
-
|
|
6841
|
-
|
|
6840
|
+
const imageLimit = options.model?.replace(/^bytedance\//, "") === "seedance-2.5" ? 30 : 9;
|
|
6841
|
+
if (options.referenceImageUrls.length > imageLimit) {
|
|
6842
|
+
throw new Error(`referenceImageUrls accepts at most ${imageLimit} images.`);
|
|
6843
|
+
}
|
|
6844
|
+
}
|
|
6845
|
+
for (const [field, clips] of [
|
|
6846
|
+
["referenceVideos", options?.referenceVideos],
|
|
6847
|
+
["referenceAudios", options?.referenceAudios]
|
|
6848
|
+
]) {
|
|
6849
|
+
if (clips === void 0) continue;
|
|
6850
|
+
if (clips.length < 1 || clips.length > 3) {
|
|
6851
|
+
throw new Error(`${field} accepts 1 to 3 clips.`);
|
|
6852
|
+
}
|
|
6853
|
+
for (const clip of clips) {
|
|
6854
|
+
if (typeof clip?.url !== "string" || !/^https?:\/\//i.test(clip.url)) {
|
|
6855
|
+
throw new Error(`${field} URLs must be http(s).`);
|
|
6856
|
+
}
|
|
6857
|
+
if (clip.role !== void 0 && clip.role !== "reference") {
|
|
6858
|
+
throw new Error(`${field} role must be "reference" (or omitted).`);
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
if (options?.referenceVideos?.length || options?.referenceAudios?.length) {
|
|
6863
|
+
if (options.imageUrl || options.lastFrameUrl || options.realFaceAssetId) {
|
|
6864
|
+
throw new Error(
|
|
6865
|
+
"referenceVideos / referenceAudios are mutually exclusive with imageUrl, lastFrameUrl, and realFaceAssetId; combine them with referenceImageUrls instead."
|
|
6866
|
+
);
|
|
6867
|
+
}
|
|
6868
|
+
if (options.referenceAudios?.length && !options.referenceImageUrls?.length && !options.referenceVideos?.length) {
|
|
6869
|
+
throw new Error("referenceAudios requires referenceImageUrls or referenceVideos.");
|
|
6842
6870
|
}
|
|
6843
6871
|
}
|
|
6844
6872
|
const body = {
|
|
@@ -6855,7 +6883,14 @@ var VideoClient = class {
|
|
|
6855
6883
|
if (options?.generateAudio !== void 0) body.generate_audio = options.generateAudio;
|
|
6856
6884
|
if (options?.seed !== void 0) body.seed = options.seed;
|
|
6857
6885
|
if (options?.watermark !== void 0) body.watermark = options.watermark;
|
|
6858
|
-
if (options?.returnLastFrame) body.return_last_frame =
|
|
6886
|
+
if (options?.returnLastFrame !== void 0) body.return_last_frame = options.returnLastFrame;
|
|
6887
|
+
if (options?.referenceVideos !== void 0) body.reference_videos = options.referenceVideos;
|
|
6888
|
+
if (options?.referenceAudios !== void 0) body.reference_audios = options.referenceAudios;
|
|
6889
|
+
if (options?.bitrateMode !== void 0) body.bitrate_mode = options.bitrateMode;
|
|
6890
|
+
if (options?.outputFormat !== void 0) body.output_format = options.outputFormat;
|
|
6891
|
+
if (options?.cameraFixed !== void 0) body.camera_fixed = options.cameraFixed;
|
|
6892
|
+
if (options?.safetyIdentifier !== void 0) body.safety_identifier = options.safetyIdentifier;
|
|
6893
|
+
if (options?.inputType !== void 0) body.input_type = options.inputType;
|
|
6859
6894
|
const budgetMs = options?.budgetMs ?? DEFAULT_GENERATE_BUDGET_MS;
|
|
6860
6895
|
return this.submitAndPoll(body, budgetMs);
|
|
6861
6896
|
}
|
|
@@ -6863,8 +6898,11 @@ var VideoClient = class {
|
|
|
6863
6898
|
* Generate a video from a standard Seedance `content[]` body.
|
|
6864
6899
|
*
|
|
6865
6900
|
* Targets the gateway's `POST /v1/videos` endpoint, which accepts the
|
|
6866
|
-
* mainstream multimodal `content` array
|
|
6867
|
-
*
|
|
6901
|
+
* mainstream multimodal `content` array used by other Seedance APIs. Items
|
|
6902
|
+
* may carry a `role` — `first_frame`, `last_frame`, `reference_image`,
|
|
6903
|
+
* `reference_video`, `reference_audio` — which the gateway maps to the same
|
|
6904
|
+
* validated fields as {@link generate}; a role-less single image keeps its
|
|
6905
|
+
* first-frame meaning — so callers already holding a
|
|
6868
6906
|
* `content[]`-shaped request can submit it unchanged. The gateway validates
|
|
6869
6907
|
* unsupported inputs *before* charging, then delegates to the same x402
|
|
6870
6908
|
* submit+poll pipeline as {@link generate}.
|
|
@@ -6878,7 +6916,8 @@ var VideoClient = class {
|
|
|
6878
6916
|
* `{ type: "image_url", image_url: { url: "https://..." } }`.
|
|
6879
6917
|
* @param options - `model`, `budgetMs`, plus the same camelCase render options
|
|
6880
6918
|
* as {@link generate} (`durationSeconds`, `aspectRatio`, `resolution`,
|
|
6881
|
-
* `generateAudio`, `seed`, `watermark`, `returnLastFrame`
|
|
6919
|
+
* `generateAudio`, `seed`, `watermark`, `returnLastFrame`, `bitrateMode`,
|
|
6920
|
+
* `outputFormat`, `cameraFixed`, `safetyIdentifier`, `inputType`). These are mapped
|
|
6882
6921
|
* to the gateway's snake_case fields for you. Any other keys you pass are
|
|
6883
6922
|
* forwarded verbatim (use snake_case for those, since the gateway reads
|
|
6884
6923
|
* snake_case only).
|
|
@@ -6897,6 +6936,11 @@ var VideoClient = class {
|
|
|
6897
6936
|
seed,
|
|
6898
6937
|
watermark,
|
|
6899
6938
|
returnLastFrame,
|
|
6939
|
+
bitrateMode,
|
|
6940
|
+
outputFormat,
|
|
6941
|
+
cameraFixed,
|
|
6942
|
+
safetyIdentifier,
|
|
6943
|
+
inputType,
|
|
6900
6944
|
...extra
|
|
6901
6945
|
} = options ?? {};
|
|
6902
6946
|
const body = { ...extra, content };
|
|
@@ -6908,6 +6952,11 @@ var VideoClient = class {
|
|
|
6908
6952
|
if (seed !== void 0) body.seed = seed;
|
|
6909
6953
|
if (watermark !== void 0) body.watermark = watermark;
|
|
6910
6954
|
if (returnLastFrame !== void 0) body.return_last_frame = returnLastFrame;
|
|
6955
|
+
if (bitrateMode !== void 0) body.bitrate_mode = bitrateMode;
|
|
6956
|
+
if (outputFormat !== void 0) body.output_format = outputFormat;
|
|
6957
|
+
if (cameraFixed !== void 0) body.camera_fixed = cameraFixed;
|
|
6958
|
+
if (safetyIdentifier !== void 0) body.safety_identifier = safetyIdentifier;
|
|
6959
|
+
if (inputType !== void 0) body.input_type = inputType;
|
|
6911
6960
|
return this.submitAndPoll(body, budgetMs ?? DEFAULT_GENERATE_BUDGET_MS, "/v1/videos");
|
|
6912
6961
|
}
|
|
6913
6962
|
// --------------------------------------------------------------------
|
|
@@ -7444,8 +7493,8 @@ var PhoneClient = class {
|
|
|
7444
7493
|
return data;
|
|
7445
7494
|
}
|
|
7446
7495
|
// ─── Internals ─────────────────────────────────────────────────────────
|
|
7447
|
-
async request(
|
|
7448
|
-
const url = `${this.apiUrl}/v1/phone/${
|
|
7496
|
+
async request(path7, body) {
|
|
7497
|
+
const url = `${this.apiUrl}/v1/phone/${path7}`;
|
|
7449
7498
|
const response = await this.fetchWithTimeout(url, {
|
|
7450
7499
|
method: "POST",
|
|
7451
7500
|
headers: { "Content-Type": "application/json" },
|
|
@@ -7886,10 +7935,10 @@ var PriceClient = class {
|
|
|
7886
7935
|
}
|
|
7887
7936
|
async price(category, symbol, options) {
|
|
7888
7937
|
if (!symbol) throw new Error("symbol is required");
|
|
7889
|
-
const
|
|
7938
|
+
const path7 = categoryPath(category, options?.market, "price", symbol);
|
|
7890
7939
|
const query = {};
|
|
7891
7940
|
if (options?.session) query.session = options.session;
|
|
7892
|
-
const data = await this.getWithPayment(
|
|
7941
|
+
const data = await this.getWithPayment(path7, query);
|
|
7893
7942
|
return {
|
|
7894
7943
|
symbol: data.symbol ?? symbol.toUpperCase(),
|
|
7895
7944
|
price: data.price,
|
|
@@ -7908,14 +7957,14 @@ var PriceClient = class {
|
|
|
7908
7957
|
if (!options.from || options.from <= 0) {
|
|
7909
7958
|
throw new Error("history requires options.from (unix seconds)");
|
|
7910
7959
|
}
|
|
7911
|
-
const
|
|
7960
|
+
const path7 = categoryPath(category, options.market, "history", symbol);
|
|
7912
7961
|
const query = {
|
|
7913
7962
|
resolution: options.resolution ?? "D",
|
|
7914
7963
|
from: String(options.from)
|
|
7915
7964
|
};
|
|
7916
7965
|
if (options.to) query.to = String(options.to);
|
|
7917
7966
|
if (options.session) query.session = options.session;
|
|
7918
|
-
const data = await this.getWithPayment(
|
|
7967
|
+
const data = await this.getWithPayment(path7, query);
|
|
7919
7968
|
return {
|
|
7920
7969
|
symbol: data.symbol ?? symbol.toUpperCase(),
|
|
7921
7970
|
resolution: data.resolution ?? (options.resolution ?? "D"),
|
|
@@ -7927,12 +7976,12 @@ var PriceClient = class {
|
|
|
7927
7976
|
};
|
|
7928
7977
|
}
|
|
7929
7978
|
async listSymbols(category, options) {
|
|
7930
|
-
const
|
|
7979
|
+
const path7 = categoryPath(category, options?.market, "list");
|
|
7931
7980
|
const query = {
|
|
7932
7981
|
limit: String(options?.limit && options.limit > 0 ? options.limit : 100)
|
|
7933
7982
|
};
|
|
7934
7983
|
if (options?.query) query.q = options.query;
|
|
7935
|
-
const data = await this.getWithPayment(
|
|
7984
|
+
const data = await this.getWithPayment(path7, query);
|
|
7936
7985
|
if (Array.isArray(data)) {
|
|
7937
7986
|
return { symbols: data, count: data.length };
|
|
7938
7987
|
}
|
|
@@ -8105,19 +8154,19 @@ var SurfClient = class {
|
|
|
8105
8154
|
* `/v1/surf` is tolerated and stripped). Query params are URL-encoded;
|
|
8106
8155
|
* arrays become repeated keys (`?a=1&a=2`).
|
|
8107
8156
|
*/
|
|
8108
|
-
async get(
|
|
8109
|
-
const url = this.buildUrl(
|
|
8157
|
+
async get(path7, params) {
|
|
8158
|
+
const url = this.buildUrl(path7, params);
|
|
8110
8159
|
return this.requestWithPayment(url, "GET");
|
|
8111
8160
|
}
|
|
8112
8161
|
/**
|
|
8113
8162
|
* POST a Surf endpoint with a JSON body. Same path normalization as `get`.
|
|
8114
8163
|
*/
|
|
8115
|
-
async post(
|
|
8116
|
-
const url = this.buildUrl(
|
|
8164
|
+
async post(path7, body) {
|
|
8165
|
+
const url = this.buildUrl(path7);
|
|
8117
8166
|
return this.requestWithPayment(url, "POST", body);
|
|
8118
8167
|
}
|
|
8119
|
-
buildUrl(
|
|
8120
|
-
let normalized =
|
|
8168
|
+
buildUrl(path7, params) {
|
|
8169
|
+
let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
|
|
8121
8170
|
if (!normalized.startsWith(SURF_PREFIX)) {
|
|
8122
8171
|
normalized = `${SURF_PREFIX}${normalized}`;
|
|
8123
8172
|
}
|
|
@@ -8575,15 +8624,15 @@ var BlockrunClient = class {
|
|
|
8575
8624
|
* `/api` is tolerated and stripped). Query params are URL-encoded; arrays
|
|
8576
8625
|
* become repeated keys (`?a=1&a=2`); undefined/null are dropped.
|
|
8577
8626
|
*/
|
|
8578
|
-
async get(
|
|
8579
|
-
const url = this.buildUrl(
|
|
8627
|
+
async get(path7, params) {
|
|
8628
|
+
const url = this.buildUrl(path7, params);
|
|
8580
8629
|
return this.requestWithPayment(url, "GET");
|
|
8581
8630
|
}
|
|
8582
8631
|
/**
|
|
8583
8632
|
* POST a BlockRun endpoint with a JSON body.
|
|
8584
8633
|
*/
|
|
8585
|
-
async post(
|
|
8586
|
-
const url = this.buildUrl(
|
|
8634
|
+
async post(path7, body) {
|
|
8635
|
+
const url = this.buildUrl(path7);
|
|
8587
8636
|
return this.requestWithPayment(url, "POST", body);
|
|
8588
8637
|
}
|
|
8589
8638
|
/**
|
|
@@ -8607,8 +8656,8 @@ var BlockrunClient = class {
|
|
|
8607
8656
|
* short-circuits and returns the body. Most long-running endpoints (image,
|
|
8608
8657
|
* video, music, voice) return 202 with a poll_url.
|
|
8609
8658
|
*/
|
|
8610
|
-
async poll(
|
|
8611
|
-
const submitUrl = this.buildUrl(
|
|
8659
|
+
async poll(path7, body, options) {
|
|
8660
|
+
const submitUrl = this.buildUrl(path7);
|
|
8612
8661
|
const budgetMs = options?.budgetMs ?? DEFAULT_POLL_BUDGET_MS;
|
|
8613
8662
|
const intervalMs = options?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
8614
8663
|
const resp402 = await this.fetchWithTimeout(submitUrl, {
|
|
@@ -8708,8 +8757,8 @@ var BlockrunClient = class {
|
|
|
8708
8757
|
* process.stdout.write(chunk.choices?.[0]?.delta?.content ?? "");
|
|
8709
8758
|
* }
|
|
8710
8759
|
*/
|
|
8711
|
-
async *stream(
|
|
8712
|
-
const url = this.buildUrl(
|
|
8760
|
+
async *stream(path7, body) {
|
|
8761
|
+
const url = this.buildUrl(path7);
|
|
8713
8762
|
const requestBody = JSON.stringify(body ?? {});
|
|
8714
8763
|
const resp402 = await this.fetchWithTimeout(url, {
|
|
8715
8764
|
method: "POST",
|
|
@@ -8752,8 +8801,8 @@ var BlockrunClient = class {
|
|
|
8752
8801
|
// --------------------------------------------------------------------
|
|
8753
8802
|
// Internal: shared infrastructure
|
|
8754
8803
|
// --------------------------------------------------------------------
|
|
8755
|
-
buildUrl(
|
|
8756
|
-
let normalized =
|
|
8804
|
+
buildUrl(path7, params) {
|
|
8805
|
+
let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
|
|
8757
8806
|
if (normalized.startsWith("/api/")) {
|
|
8758
8807
|
normalized = normalized.slice(4);
|
|
8759
8808
|
}
|
|
@@ -9399,6 +9448,332 @@ async function getOrCreateSolanaWallet() {
|
|
|
9399
9448
|
return { address, privateKey, isNew: true };
|
|
9400
9449
|
}
|
|
9401
9450
|
|
|
9451
|
+
// src/solana-batch.ts
|
|
9452
|
+
import * as fs6 from "fs";
|
|
9453
|
+
import * as path5 from "path";
|
|
9454
|
+
var BATCH_SCHEME = "batch-settlement";
|
|
9455
|
+
var INSTALL_HINT2 = "npm install @x402/core@~2.28.0 @x402/svm@~2.28.0 @solana/kit";
|
|
9456
|
+
async function load(pkg, importer) {
|
|
9457
|
+
try {
|
|
9458
|
+
return await importer();
|
|
9459
|
+
} catch (err) {
|
|
9460
|
+
throw new Error(
|
|
9461
|
+
`@blockrun/llm: Solana batch-settlement requires the optional peer dependency "${pkg}", which is not installed.
|
|
9462
|
+
|
|
9463
|
+
${INSTALL_HINT2}
|
|
9464
|
+
|
|
9465
|
+
Original error: ${err instanceof Error ? err.message : String(err)}`,
|
|
9466
|
+
{ cause: err }
|
|
9467
|
+
);
|
|
9468
|
+
}
|
|
9469
|
+
}
|
|
9470
|
+
var FileChannelStorage = class {
|
|
9471
|
+
constructor(file) {
|
|
9472
|
+
this.file = file;
|
|
9473
|
+
}
|
|
9474
|
+
file;
|
|
9475
|
+
read() {
|
|
9476
|
+
try {
|
|
9477
|
+
const parsed = JSON.parse(fs6.readFileSync(this.file, "utf8"));
|
|
9478
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
9479
|
+
} catch (err) {
|
|
9480
|
+
if (err.code === "ENOENT") return {};
|
|
9481
|
+
throw err;
|
|
9482
|
+
}
|
|
9483
|
+
}
|
|
9484
|
+
write(all) {
|
|
9485
|
+
fs6.mkdirSync(path5.dirname(this.file), { recursive: true, mode: 448 });
|
|
9486
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
9487
|
+
fs6.writeFileSync(tmp, JSON.stringify(all, null, 2), { mode: 384 });
|
|
9488
|
+
fs6.renameSync(tmp, this.file);
|
|
9489
|
+
}
|
|
9490
|
+
async get(key) {
|
|
9491
|
+
return this.read()[key];
|
|
9492
|
+
}
|
|
9493
|
+
async set(key, record) {
|
|
9494
|
+
const all = this.read();
|
|
9495
|
+
all[key] = record;
|
|
9496
|
+
this.write(all);
|
|
9497
|
+
}
|
|
9498
|
+
async delete(key) {
|
|
9499
|
+
const all = this.read();
|
|
9500
|
+
if (!(key in all)) return;
|
|
9501
|
+
delete all[key];
|
|
9502
|
+
this.write(all);
|
|
9503
|
+
}
|
|
9504
|
+
/** Forget every channel in this file. */
|
|
9505
|
+
clear() {
|
|
9506
|
+
try {
|
|
9507
|
+
fs6.unlinkSync(this.file);
|
|
9508
|
+
} catch (err) {
|
|
9509
|
+
if (err.code !== "ENOENT") throw err;
|
|
9510
|
+
}
|
|
9511
|
+
}
|
|
9512
|
+
};
|
|
9513
|
+
function processAlive(pid) {
|
|
9514
|
+
try {
|
|
9515
|
+
process.kill(pid, 0);
|
|
9516
|
+
return true;
|
|
9517
|
+
} catch (err) {
|
|
9518
|
+
return err.code === "EPERM";
|
|
9519
|
+
}
|
|
9520
|
+
}
|
|
9521
|
+
var heldLocks = /* @__PURE__ */ new Set();
|
|
9522
|
+
var exitHookInstalled = false;
|
|
9523
|
+
function lockChannelFile(file) {
|
|
9524
|
+
if (heldLocks.has(file)) return void 0;
|
|
9525
|
+
const lock = `${file}.lock`;
|
|
9526
|
+
fs6.mkdirSync(path5.dirname(lock), { recursive: true, mode: 448 });
|
|
9527
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
9528
|
+
try {
|
|
9529
|
+
fs6.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
9530
|
+
heldLocks.add(file);
|
|
9531
|
+
if (!exitHookInstalled) {
|
|
9532
|
+
exitHookInstalled = true;
|
|
9533
|
+
process.once("exit", () => {
|
|
9534
|
+
for (const held of heldLocks) {
|
|
9535
|
+
try {
|
|
9536
|
+
fs6.unlinkSync(`${held}.lock`);
|
|
9537
|
+
} catch {
|
|
9538
|
+
}
|
|
9539
|
+
}
|
|
9540
|
+
});
|
|
9541
|
+
}
|
|
9542
|
+
return void 0;
|
|
9543
|
+
} catch (err) {
|
|
9544
|
+
if (err.code !== "EEXIST") throw err;
|
|
9545
|
+
}
|
|
9546
|
+
const owner = Number(fs6.readFileSync(lock, "utf8").trim());
|
|
9547
|
+
if (Number.isInteger(owner) && owner > 0 && owner !== process.pid && processAlive(owner)) return owner;
|
|
9548
|
+
try {
|
|
9549
|
+
fs6.unlinkSync(lock);
|
|
9550
|
+
} catch {
|
|
9551
|
+
}
|
|
9552
|
+
}
|
|
9553
|
+
return -1;
|
|
9554
|
+
}
|
|
9555
|
+
function pendingDeposit(pending) {
|
|
9556
|
+
const entries = Array.isArray(pending) ? pending : [pending];
|
|
9557
|
+
return entries.some(
|
|
9558
|
+
(entry) => entry?.payment?.payload?.type === "deposit"
|
|
9559
|
+
);
|
|
9560
|
+
}
|
|
9561
|
+
function dropOrphanedPending(storage) {
|
|
9562
|
+
return {
|
|
9563
|
+
get: async (key) => {
|
|
9564
|
+
const record = await storage.get(key);
|
|
9565
|
+
if (!record?.pending || record.channelConfig?.voucherSigner !== "server") return record;
|
|
9566
|
+
if (!record.hasConfirmedState || pendingDeposit(record.pending)) {
|
|
9567
|
+
await storage.delete(key);
|
|
9568
|
+
return void 0;
|
|
9569
|
+
}
|
|
9570
|
+
const { pending: _pending, hasConfirmedState: _confirmed, ...confirmed } = record;
|
|
9571
|
+
await storage.set(key, confirmed);
|
|
9572
|
+
return confirmed;
|
|
9573
|
+
},
|
|
9574
|
+
set: (key, record) => storage.set(key, record),
|
|
9575
|
+
delete: (key) => storage.delete(key)
|
|
9576
|
+
};
|
|
9577
|
+
}
|
|
9578
|
+
function offersBatch(paymentRequired) {
|
|
9579
|
+
return (paymentRequired.accepts ?? []).some((accept) => accept.scheme === BATCH_SCHEME);
|
|
9580
|
+
}
|
|
9581
|
+
function atomicToUsd(amount) {
|
|
9582
|
+
return amount !== void 0 && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
|
|
9583
|
+
}
|
|
9584
|
+
async function isBatchRefusal(response) {
|
|
9585
|
+
if (response.status === 402) return "payment_required";
|
|
9586
|
+
if (![400, 403, 409, 503].includes(response.status)) return void 0;
|
|
9587
|
+
let body;
|
|
9588
|
+
try {
|
|
9589
|
+
body = await response.clone().json();
|
|
9590
|
+
} catch {
|
|
9591
|
+
return void 0;
|
|
9592
|
+
}
|
|
9593
|
+
const { error, code } = body ?? {};
|
|
9594
|
+
if (code === "PAYMENT_VERIFICATION_UNAVAILABLE") return code;
|
|
9595
|
+
return typeof error === "string" && error.startsWith("batch_") ? error : void 0;
|
|
9596
|
+
}
|
|
9597
|
+
var wallets = /* @__PURE__ */ new Map();
|
|
9598
|
+
var SolanaBatchPayer = class {
|
|
9599
|
+
constructor(init) {
|
|
9600
|
+
this.init = init;
|
|
9601
|
+
const { operators } = init.options;
|
|
9602
|
+
if (!Array.isArray(operators) || operators.length === 0 || operators.some((o) => typeof o !== "string" || !o)) {
|
|
9603
|
+
throw new Error("batch.operators must list at least one base58 operator public key");
|
|
9604
|
+
}
|
|
9605
|
+
}
|
|
9606
|
+
init;
|
|
9607
|
+
state;
|
|
9608
|
+
async storeFile() {
|
|
9609
|
+
const { channelStore } = this.init.options;
|
|
9610
|
+
if (channelStore === false) return void 0;
|
|
9611
|
+
return path5.resolve(channelStore ?? path5.join(paths().dir, "solana-batch", `${await this.init.address()}.json`));
|
|
9612
|
+
}
|
|
9613
|
+
/**
|
|
9614
|
+
* This wallet's shared batch state, or why this client cannot use batch:
|
|
9615
|
+
* another client in this process holds the wallet with a different trust
|
|
9616
|
+
* configuration, or another live process owns its channel file.
|
|
9617
|
+
*/
|
|
9618
|
+
wallet() {
|
|
9619
|
+
this.state ??= (async () => {
|
|
9620
|
+
const address = await this.init.address();
|
|
9621
|
+
const file = await this.storeFile();
|
|
9622
|
+
const { operators, maxDeposit } = this.init.options;
|
|
9623
|
+
const config = JSON.stringify([[...operators].sort(), String(maxDeposit ?? ""), file ?? null, this.init.rpcUrl]);
|
|
9624
|
+
const existing = wallets.get(address);
|
|
9625
|
+
if (existing) {
|
|
9626
|
+
return existing.config === config ? existing : "another client in this process uses this wallet with different batch options";
|
|
9627
|
+
}
|
|
9628
|
+
if (file) {
|
|
9629
|
+
const owner = lockChannelFile(file);
|
|
9630
|
+
if (owner !== void 0) return `channel store ${file} is in use by process ${owner}`;
|
|
9631
|
+
}
|
|
9632
|
+
const created = { config, busy: false, warned: /* @__PURE__ */ new Set() };
|
|
9633
|
+
wallets.set(address, created);
|
|
9634
|
+
return created;
|
|
9635
|
+
})();
|
|
9636
|
+
return this.state;
|
|
9637
|
+
}
|
|
9638
|
+
build(wallet) {
|
|
9639
|
+
wallet.client ??= (async () => {
|
|
9640
|
+
const [core, svm, kit] = await Promise.all([
|
|
9641
|
+
load("@x402/core", () => import("@x402/core/client")),
|
|
9642
|
+
load("@x402/svm", () => import("@x402/svm/batch-settlement/client")),
|
|
9643
|
+
load("@solana/kit", () => import("@solana/kit"))
|
|
9644
|
+
]);
|
|
9645
|
+
const signer = await kit.createKeyPairSignerFromBytes(await this.init.secretKey());
|
|
9646
|
+
const file = await this.storeFile();
|
|
9647
|
+
const storage = file ? dropOrphanedPending(new FileChannelStorage(file)) : void 0;
|
|
9648
|
+
const scheme = new svm.BatchSvmScheme(signer, {
|
|
9649
|
+
rpcUrl: this.init.rpcUrl,
|
|
9650
|
+
serverSignedChannelsPolicy: {
|
|
9651
|
+
allowedOperators: this.init.options.operators,
|
|
9652
|
+
maxDeposit: this.init.options.maxDeposit ?? svm.DEFAULT_SERVER_SIGNED_MAX_DEPOSIT
|
|
9653
|
+
},
|
|
9654
|
+
...storage ? { channelStorage: storage } : {}
|
|
9655
|
+
});
|
|
9656
|
+
const client = new core.x402Client().register("solana:*", scheme).registerPolicy(scheme.paymentPolicy);
|
|
9657
|
+
client.setSpendControls({ maxAmountPerPayment: false });
|
|
9658
|
+
return {
|
|
9659
|
+
http: new core.x402HTTPClient(client),
|
|
9660
|
+
refund: (url) => scheme.refund(url)
|
|
9661
|
+
};
|
|
9662
|
+
})();
|
|
9663
|
+
wallet.client.catch(() => {
|
|
9664
|
+
wallet.client = void 0;
|
|
9665
|
+
});
|
|
9666
|
+
return wallet.client;
|
|
9667
|
+
}
|
|
9668
|
+
/**
|
|
9669
|
+
* Drop everything this wallet knows about its channel, on disk and in
|
|
9670
|
+
* memory. The next call rebuilds the scheme, which reads the channel's real
|
|
9671
|
+
* state (deposit, open or closed) from the chain.
|
|
9672
|
+
*/
|
|
9673
|
+
async forget(wallet) {
|
|
9674
|
+
wallet.client = void 0;
|
|
9675
|
+
const file = await this.storeFile();
|
|
9676
|
+
if (file) new FileChannelStorage(file).clear();
|
|
9677
|
+
}
|
|
9678
|
+
warn(wallet, reason) {
|
|
9679
|
+
if (wallet?.warned.has(reason)) return;
|
|
9680
|
+
wallet?.warned.add(reason);
|
|
9681
|
+
console.error(`[@blockrun/llm] batch-settlement unavailable (${reason}); paying with exact`);
|
|
9682
|
+
}
|
|
9683
|
+
/**
|
|
9684
|
+
* Pay one 402 with batch-settlement, or say why the caller should use exact.
|
|
9685
|
+
*
|
|
9686
|
+
* `send` replays the original request with the payment headers. The
|
|
9687
|
+
* returned response is 2xx; any other answer either becomes a fallback (the
|
|
9688
|
+
* gateway charged nothing) or is returned to the caller's own error path.
|
|
9689
|
+
*/
|
|
9690
|
+
async pay(paymentRequired, send) {
|
|
9691
|
+
if (!offersBatch(paymentRequired)) return { kind: "fallback", reason: "not_offered" };
|
|
9692
|
+
const wallet = await this.wallet();
|
|
9693
|
+
if (typeof wallet === "string") {
|
|
9694
|
+
this.warn(void 0, wallet);
|
|
9695
|
+
return { kind: "fallback", reason: wallet };
|
|
9696
|
+
}
|
|
9697
|
+
if (wallet.busy) return { kind: "fallback", reason: "channel_busy" };
|
|
9698
|
+
wallet.busy = true;
|
|
9699
|
+
try {
|
|
9700
|
+
let http;
|
|
9701
|
+
let payload;
|
|
9702
|
+
try {
|
|
9703
|
+
http = (await this.build(wallet)).http;
|
|
9704
|
+
payload = await http.createPaymentPayload(paymentRequired);
|
|
9705
|
+
} catch (err) {
|
|
9706
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
9707
|
+
this.warn(wallet, reason);
|
|
9708
|
+
return { kind: "fallback", reason };
|
|
9709
|
+
}
|
|
9710
|
+
const deposit = payload.payload?.type === "deposit";
|
|
9711
|
+
let response;
|
|
9712
|
+
try {
|
|
9713
|
+
response = await send(http.encodePaymentSignatureHeader(payload));
|
|
9714
|
+
} catch (err) {
|
|
9715
|
+
if (deposit) await this.forget(wallet);
|
|
9716
|
+
else await this.settle(http, payload, () => null, 0);
|
|
9717
|
+
throw err;
|
|
9718
|
+
}
|
|
9719
|
+
const getHeader = (name) => response.headers.get(name);
|
|
9720
|
+
const settled = await this.settle(http, payload, getHeader, response.status);
|
|
9721
|
+
if (deposit && !(response.ok && settled?.success === true)) await this.forget(wallet);
|
|
9722
|
+
if (response.ok) {
|
|
9723
|
+
const charged = atomicToUsd(settled?.amount) ?? atomicToUsd(payload.accepted?.amount) ?? 0;
|
|
9724
|
+
return { kind: "paid", response, chargedUsd: charged };
|
|
9725
|
+
}
|
|
9726
|
+
const refusal = await isBatchRefusal(response);
|
|
9727
|
+
if (refusal) {
|
|
9728
|
+
this.warn(wallet, refusal);
|
|
9729
|
+
return { kind: "fallback", reason: refusal };
|
|
9730
|
+
}
|
|
9731
|
+
return { kind: "failed", response };
|
|
9732
|
+
} finally {
|
|
9733
|
+
wallet.busy = false;
|
|
9734
|
+
}
|
|
9735
|
+
}
|
|
9736
|
+
/**
|
|
9737
|
+
* Hand the gateway's answer to the scheme, which verifies the operator's
|
|
9738
|
+
* voucher and advances (or rolls back) the channel's local state.
|
|
9739
|
+
*
|
|
9740
|
+
* A malformed receipt must not lose a response the caller has been served,
|
|
9741
|
+
* so a scheme error is logged rather than thrown.
|
|
9742
|
+
*/
|
|
9743
|
+
async settle(http, payload, getHeader, status2) {
|
|
9744
|
+
try {
|
|
9745
|
+
return (await http.processPaymentResult(payload, getHeader, status2)).settleResponse;
|
|
9746
|
+
} catch (err) {
|
|
9747
|
+
console.error(
|
|
9748
|
+
`[@blockrun/llm] batch-settlement receipt not reconciled: ${err instanceof Error ? err.message : String(err)}`
|
|
9749
|
+
);
|
|
9750
|
+
return void 0;
|
|
9751
|
+
}
|
|
9752
|
+
}
|
|
9753
|
+
/**
|
|
9754
|
+
* Close the channel and return its unused escrow to the wallet.
|
|
9755
|
+
*
|
|
9756
|
+
* The scheme keeps a closed channel in its records, and would go on paying
|
|
9757
|
+
* into it, so a successful close forgets the channel. The next batch call
|
|
9758
|
+
* finds no open channel on-chain and opens a new one.
|
|
9759
|
+
*
|
|
9760
|
+
* @param url - any batch-enabled route on the gateway the channel was opened with.
|
|
9761
|
+
*/
|
|
9762
|
+
async close(url) {
|
|
9763
|
+
const wallet = await this.wallet();
|
|
9764
|
+
if (typeof wallet === "string") throw new Error(`batch-settlement unavailable: ${wallet}`);
|
|
9765
|
+
if (wallet.busy) throw new Error("batch-settlement channel has a request in flight; close it when the call returns");
|
|
9766
|
+
wallet.busy = true;
|
|
9767
|
+
try {
|
|
9768
|
+
const result = await (await this.build(wallet)).refund(url);
|
|
9769
|
+
await this.forget(wallet);
|
|
9770
|
+
return result;
|
|
9771
|
+
} finally {
|
|
9772
|
+
wallet.busy = false;
|
|
9773
|
+
}
|
|
9774
|
+
}
|
|
9775
|
+
};
|
|
9776
|
+
|
|
9402
9777
|
// src/solana-client.ts
|
|
9403
9778
|
var SOLANA_API_URL = "https://sol.blockrun.ai/api";
|
|
9404
9779
|
var DEFAULT_MAX_TOKENS2 = 1024;
|
|
@@ -9462,7 +9837,7 @@ async function isSafeStaleBlockhashResponse(response) {
|
|
|
9462
9837
|
}
|
|
9463
9838
|
async function waitForStaleRetry(attempt) {
|
|
9464
9839
|
await new Promise(
|
|
9465
|
-
(
|
|
9840
|
+
(resolve2) => setTimeout(resolve2, STALE_BLOCKHASH_RETRY_BACKOFFS_MS[attempt])
|
|
9466
9841
|
);
|
|
9467
9842
|
}
|
|
9468
9843
|
var DEFAULT_SOLANA_RPC_URL = "https://sol.blockrun.ai/api/v1/solana/rpc";
|
|
@@ -9506,6 +9881,7 @@ var SolanaLLMClient = class {
|
|
|
9506
9881
|
addressCache = null;
|
|
9507
9882
|
modelPricingCache = null;
|
|
9508
9883
|
modelPricingPromise = null;
|
|
9884
|
+
batchPayer;
|
|
9509
9885
|
constructor(options = {}) {
|
|
9510
9886
|
this.apiAuth = resolveApiKeyAuth(options);
|
|
9511
9887
|
if (!this.apiAuth) {
|
|
@@ -9525,6 +9901,25 @@ var SolanaLLMClient = class {
|
|
|
9525
9901
|
this.rpcUrl = rpc.url;
|
|
9526
9902
|
this.rpcHeaders = rpc.headers;
|
|
9527
9903
|
this.timeout = options.timeout || DEFAULT_TIMEOUT14;
|
|
9904
|
+
if (options.batch && !this.apiAuth) {
|
|
9905
|
+
this.batchPayer = new SolanaBatchPayer({
|
|
9906
|
+
options: options.batch,
|
|
9907
|
+
secretKey: () => solanaKeyToBytes(this.privateKey),
|
|
9908
|
+
address: () => this.getWalletAddress(),
|
|
9909
|
+
rpcUrl: this.rpcUrl
|
|
9910
|
+
});
|
|
9911
|
+
}
|
|
9912
|
+
}
|
|
9913
|
+
/**
|
|
9914
|
+
* Close this wallet's batch-settlement channel and return its unused escrow.
|
|
9915
|
+
*
|
|
9916
|
+
* The gateway closes it cooperatively when it can; otherwise this starts a
|
|
9917
|
+
* payer-forced close and the escrow comes back after the channel's grace
|
|
9918
|
+
* period. Only meaningful with the `batch` option.
|
|
9919
|
+
*/
|
|
9920
|
+
async closeBatchChannel() {
|
|
9921
|
+
if (!this.batchPayer) throw new Error("closeBatchChannel() requires the `batch` option in wallet mode");
|
|
9922
|
+
return this.batchPayer.close(`${this.apiUrl}/v1/chat/completions`);
|
|
9528
9923
|
}
|
|
9529
9924
|
/** Get Solana wallet address (public key in base58). */
|
|
9530
9925
|
async getWalletAddress() {
|
|
@@ -9718,8 +10113,8 @@ var SolanaLLMClient = class {
|
|
|
9718
10113
|
* how you end up debugging a request you did not send.
|
|
9719
10114
|
* @returns each decoded SSE frame, in order.
|
|
9720
10115
|
*/
|
|
9721
|
-
async *stream(
|
|
9722
|
-
const url = this.buildUrl(
|
|
10116
|
+
async *stream(path7, body) {
|
|
10117
|
+
const url = this.buildUrl(path7);
|
|
9723
10118
|
const response = await this.openPaidStream(url, JSON.stringify(body ?? {}));
|
|
9724
10119
|
yield* readSseFrames(
|
|
9725
10120
|
response,
|
|
@@ -9791,8 +10186,8 @@ var SolanaLLMClient = class {
|
|
|
9791
10186
|
* @param path - endpoint path, with or without a leading slash.
|
|
9792
10187
|
* @returns the absolute URL to call.
|
|
9793
10188
|
*/
|
|
9794
|
-
buildUrl(
|
|
9795
|
-
let normalized =
|
|
10189
|
+
buildUrl(path7) {
|
|
10190
|
+
let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
|
|
9796
10191
|
if (normalized.startsWith("/api/")) normalized = normalized.slice(4);
|
|
9797
10192
|
return `${this.apiUrl}${normalized}`;
|
|
9798
10193
|
}
|
|
@@ -9871,11 +10266,11 @@ var SolanaLLMClient = class {
|
|
|
9871
10266
|
return data;
|
|
9872
10267
|
}
|
|
9873
10268
|
// ── Prediction Markets (Powered by Predexon) ────────────────────────────
|
|
9874
|
-
async pm(
|
|
9875
|
-
return this.getWithPaymentRaw(`/v1/pm/${
|
|
10269
|
+
async pm(path7, params) {
|
|
10270
|
+
return this.getWithPaymentRaw(`/v1/pm/${path7}`, params);
|
|
9876
10271
|
}
|
|
9877
|
-
async pmQuery(
|
|
9878
|
-
return this.requestWithPaymentRaw(`/v1/pm/${
|
|
10272
|
+
async pmQuery(path7, query) {
|
|
10273
|
+
return this.requestWithPaymentRaw(`/v1/pm/${path7}`, query);
|
|
9879
10274
|
}
|
|
9880
10275
|
// ── Exa Web Search (Powered by Exa) ──────────────────────────────────────
|
|
9881
10276
|
/**
|
|
@@ -9887,8 +10282,8 @@ var SolanaLLMClient = class {
|
|
|
9887
10282
|
* @example
|
|
9888
10283
|
* const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
|
|
9889
10284
|
*/
|
|
9890
|
-
async exa(
|
|
9891
|
-
return this.requestWithPaymentRaw(`/v1/exa/${
|
|
10285
|
+
async exa(path7, body) {
|
|
10286
|
+
return this.requestWithPaymentRaw(`/v1/exa/${path7}`, body);
|
|
9892
10287
|
}
|
|
9893
10288
|
/**
|
|
9894
10289
|
* Neural and keyword web search via Exa (Solana payment).
|
|
@@ -9938,8 +10333,8 @@ var SolanaLLMClient = class {
|
|
|
9938
10333
|
* "prices/coingecko:bitcoin,base:0x..."
|
|
9939
10334
|
* @param params - Query parameters passed through to DefiLlama
|
|
9940
10335
|
*/
|
|
9941
|
-
async defi(
|
|
9942
|
-
return this.getWithPaymentRaw(`/v1/defillama/${
|
|
10336
|
+
async defi(path7, params) {
|
|
10337
|
+
return this.getWithPaymentRaw(`/v1/defillama/${path7}`, params);
|
|
9943
10338
|
}
|
|
9944
10339
|
/** All DeFi protocols with TVL. */
|
|
9945
10340
|
async defiProtocols() {
|
|
@@ -9975,9 +10370,9 @@ var SolanaLLMClient = class {
|
|
|
9975
10370
|
* @param params - Query params (chainId, sellToken, buyToken, sellAmount, taker, ...)
|
|
9976
10371
|
* @param body - JSON body — pass to switch to POST (gasless/submit only)
|
|
9977
10372
|
*/
|
|
9978
|
-
async dex(
|
|
9979
|
-
if (body) return this.requestWithPaymentRaw(`/v1/zerox/${
|
|
9980
|
-
return this.getWithPaymentRaw(`/v1/zerox/${
|
|
10373
|
+
async dex(path7, params, body) {
|
|
10374
|
+
if (body) return this.requestWithPaymentRaw(`/v1/zerox/${path7}`, body);
|
|
10375
|
+
return this.getWithPaymentRaw(`/v1/zerox/${path7}`, params);
|
|
9981
10376
|
}
|
|
9982
10377
|
/** Indicative Permit2 swap price — no commitment (free). */
|
|
9983
10378
|
async dexPrice(params) {
|
|
@@ -10021,8 +10416,8 @@ var SolanaLLMClient = class {
|
|
|
10021
10416
|
* "sandbox/status", "sandbox/terminate"
|
|
10022
10417
|
* @param body - JSON body for the endpoint
|
|
10023
10418
|
*/
|
|
10024
|
-
async modal(
|
|
10025
|
-
return this.requestWithPaymentRaw(`/v1/modal/${
|
|
10419
|
+
async modal(path7, body) {
|
|
10420
|
+
return this.requestWithPaymentRaw(`/v1/modal/${path7}`, body ?? {});
|
|
10026
10421
|
}
|
|
10027
10422
|
/** Create a sandboxed compute environment (CPU / GPU tiers). */
|
|
10028
10423
|
async modalSandboxCreate(body) {
|
|
@@ -10059,7 +10454,31 @@ var SolanaLLMClient = class {
|
|
|
10059
10454
|
});
|
|
10060
10455
|
if (response.status === 402) {
|
|
10061
10456
|
try {
|
|
10062
|
-
|
|
10457
|
+
const paymentRequired = await this.readPaymentRequired(response);
|
|
10458
|
+
if (this.batchPayer && staleRetries === 0) {
|
|
10459
|
+
const batch = await this.batchPayer.pay(
|
|
10460
|
+
paymentRequired,
|
|
10461
|
+
(paymentHeaders) => this.fetchWithTimeout(url, {
|
|
10462
|
+
method: "POST",
|
|
10463
|
+
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...paymentHeaders },
|
|
10464
|
+
body: JSON.stringify(body)
|
|
10465
|
+
})
|
|
10466
|
+
);
|
|
10467
|
+
if (batch.kind === "paid") {
|
|
10468
|
+
this.recordSettlement(batch.chargedUsd);
|
|
10469
|
+
return batch.response.json();
|
|
10470
|
+
}
|
|
10471
|
+
if (batch.kind === "failed") {
|
|
10472
|
+
let errorBody;
|
|
10473
|
+
try {
|
|
10474
|
+
errorBody = await batch.response.json();
|
|
10475
|
+
} catch {
|
|
10476
|
+
errorBody = { error: "Request failed" };
|
|
10477
|
+
}
|
|
10478
|
+
throw new APIError(`API error after payment: ${batch.response.status}`, batch.response.status, sanitizeErrorResponse(errorBody));
|
|
10479
|
+
}
|
|
10480
|
+
}
|
|
10481
|
+
return await this.handlePaymentAndRetry(url, body, paymentRequired, staleRetries > 0);
|
|
10063
10482
|
} catch (error) {
|
|
10064
10483
|
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
10065
10484
|
await waitForStaleRetry(staleRetries++);
|
|
@@ -10097,7 +10516,7 @@ var SolanaLLMClient = class {
|
|
|
10097
10516
|
* @throws PaymentError when the 402 carries no usable Solana requirements.
|
|
10098
10517
|
*/
|
|
10099
10518
|
/**
|
|
10100
|
-
* Follow a 202 `{ id, poll_url }` to completion, on Solana.
|
|
10519
|
+
* Follow a 202 `{ id, poll_url }` to completion, on Solana **image** routes.
|
|
10101
10520
|
*
|
|
10102
10521
|
* Two things differ from Base and both come from the same constraint — a
|
|
10103
10522
|
* Solana payment is a transaction pinned to a recent blockhash, valid for
|
|
@@ -10108,6 +10527,9 @@ var SolanaLLMClient = class {
|
|
|
10108
10527
|
* settles optimistically at submit, and this loop only fetches the result.
|
|
10109
10528
|
* The cost is recorded by the caller at POST, never here — recording it on
|
|
10110
10529
|
* completion would lose the charge whenever a paid job then fails.
|
|
10530
|
+
* Solana VIDEO is the exception: it settles on the completed poll and a
|
|
10531
|
+
* failed job is not charged. Do not route video through this loop as-is —
|
|
10532
|
+
* it would book spend for jobs that were never charged.
|
|
10111
10533
|
* 2. **Every poll re-signs.** One authorization cannot be replayed across a
|
|
10112
10534
|
* long render. The gateway binds the job to the payer address rather than
|
|
10113
10535
|
* to the signature, so a fresh signature from the same wallet is accepted
|
|
@@ -10177,6 +10599,15 @@ var SolanaLLMClient = class {
|
|
|
10177
10599
|
);
|
|
10178
10600
|
}
|
|
10179
10601
|
async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
|
|
10602
|
+
return this.signExactPayment(
|
|
10603
|
+
url,
|
|
10604
|
+
await this.readPaymentRequired(response),
|
|
10605
|
+
forceFreshBlockhash,
|
|
10606
|
+
resourceFallback
|
|
10607
|
+
);
|
|
10608
|
+
}
|
|
10609
|
+
/** Read a `402`'s payment requirements from its header, or its JSON body. */
|
|
10610
|
+
async readPaymentRequired(response) {
|
|
10180
10611
|
let paymentHeader = response.headers.get("payment-required");
|
|
10181
10612
|
if (!paymentHeader) {
|
|
10182
10613
|
try {
|
|
@@ -10190,8 +10621,14 @@ var SolanaLLMClient = class {
|
|
|
10190
10621
|
if (!paymentHeader) {
|
|
10191
10622
|
throw new PaymentError("402 response but no payment requirements found");
|
|
10192
10623
|
}
|
|
10193
|
-
|
|
10194
|
-
|
|
10624
|
+
return parsePaymentRequired(paymentHeader);
|
|
10625
|
+
}
|
|
10626
|
+
/** Sign the `exact` accept of a 402 as an SPL TransferChecked payment. */
|
|
10627
|
+
async signExactPayment(url, paymentRequired, forceFreshBlockhash, resourceFallback = url) {
|
|
10628
|
+
const details = extractPaymentDetails(
|
|
10629
|
+
{ ...paymentRequired, accepts: paymentRequired.accepts.filter((accept) => accept.scheme !== BATCH_SCHEME) },
|
|
10630
|
+
SOLANA_NETWORK
|
|
10631
|
+
);
|
|
10195
10632
|
if (!details.network?.startsWith("solana:")) {
|
|
10196
10633
|
throw new PaymentError(
|
|
10197
10634
|
`Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
|
|
@@ -10224,10 +10661,10 @@ var SolanaLLMClient = class {
|
|
|
10224
10661
|
);
|
|
10225
10662
|
return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
|
|
10226
10663
|
}
|
|
10227
|
-
async handlePaymentAndRetry(url, body,
|
|
10228
|
-
const { paymentPayload, costUsd } = await this.
|
|
10664
|
+
async handlePaymentAndRetry(url, body, paymentRequired, forceFreshBlockhash = false) {
|
|
10665
|
+
const { paymentPayload, costUsd } = await this.signExactPayment(
|
|
10229
10666
|
url,
|
|
10230
|
-
|
|
10667
|
+
paymentRequired,
|
|
10231
10668
|
forceFreshBlockhash,
|
|
10232
10669
|
`${this.apiUrl}/v1/chat/completions`
|
|
10233
10670
|
);
|
|
@@ -10388,13 +10825,13 @@ function solanaClient(options = {}) {
|
|
|
10388
10825
|
}
|
|
10389
10826
|
|
|
10390
10827
|
// src/cache.ts
|
|
10391
|
-
import * as
|
|
10392
|
-
import * as
|
|
10828
|
+
import * as fs7 from "fs";
|
|
10829
|
+
import * as path6 from "path";
|
|
10393
10830
|
import * as os4 from "os";
|
|
10394
10831
|
import * as crypto2 from "crypto";
|
|
10395
|
-
var CACHE_DIR =
|
|
10396
|
-
var DATA_DIR =
|
|
10397
|
-
var COST_LOG_FILE2 =
|
|
10832
|
+
var CACHE_DIR = path6.join(os4.homedir(), ".blockrun", "cache");
|
|
10833
|
+
var DATA_DIR = path6.join(os4.homedir(), ".blockrun", "data");
|
|
10834
|
+
var COST_LOG_FILE2 = path6.join(os4.homedir(), ".blockrun", "cost_log.jsonl");
|
|
10398
10835
|
var DEFAULT_TTL = {
|
|
10399
10836
|
"/v1/partner/": 3600 * 1e3,
|
|
10400
10837
|
"/v1/pm/": 1800 * 1e3,
|
|
@@ -10414,19 +10851,19 @@ function cacheKey(endpoint, body) {
|
|
|
10414
10851
|
return crypto2.createHash("sha256").update(keyData).digest("hex").slice(0, 16);
|
|
10415
10852
|
}
|
|
10416
10853
|
function cachePath(key) {
|
|
10417
|
-
return
|
|
10854
|
+
return path6.join(CACHE_DIR, `${key}.json`);
|
|
10418
10855
|
}
|
|
10419
10856
|
function getCached(key) {
|
|
10420
10857
|
const filePath = cachePath(key);
|
|
10421
|
-
if (!
|
|
10858
|
+
if (!fs7.existsSync(filePath)) return null;
|
|
10422
10859
|
try {
|
|
10423
|
-
const raw =
|
|
10860
|
+
const raw = fs7.readFileSync(filePath, "utf-8");
|
|
10424
10861
|
const entry = JSON.parse(raw);
|
|
10425
10862
|
const ttl = entry.ttlMs ?? getTtl(entry.endpoint ?? "");
|
|
10426
10863
|
if (ttl <= 0) return null;
|
|
10427
10864
|
if (Date.now() - entry.cachedAt > ttl) {
|
|
10428
10865
|
try {
|
|
10429
|
-
|
|
10866
|
+
fs7.unlinkSync(filePath);
|
|
10430
10867
|
} catch {
|
|
10431
10868
|
}
|
|
10432
10869
|
return null;
|
|
@@ -10445,7 +10882,7 @@ function getCachedByRequest(endpoint, body) {
|
|
|
10445
10882
|
function setCache(key, data, ttlMs) {
|
|
10446
10883
|
if (ttlMs <= 0) return;
|
|
10447
10884
|
try {
|
|
10448
|
-
|
|
10885
|
+
fs7.mkdirSync(CACHE_DIR, { recursive: true });
|
|
10449
10886
|
} catch {
|
|
10450
10887
|
}
|
|
10451
10888
|
const entry = {
|
|
@@ -10454,7 +10891,7 @@ function setCache(key, data, ttlMs) {
|
|
|
10454
10891
|
ttlMs
|
|
10455
10892
|
};
|
|
10456
10893
|
try {
|
|
10457
|
-
|
|
10894
|
+
fs7.writeFileSync(cachePath(key), JSON.stringify(entry));
|
|
10458
10895
|
} catch {
|
|
10459
10896
|
}
|
|
10460
10897
|
}
|
|
@@ -10475,7 +10912,7 @@ function readableFilename(endpoint, body) {
|
|
|
10475
10912
|
}
|
|
10476
10913
|
function saveReadable(endpoint, body, response, costUsd) {
|
|
10477
10914
|
try {
|
|
10478
|
-
|
|
10915
|
+
fs7.mkdirSync(DATA_DIR, { recursive: true });
|
|
10479
10916
|
} catch {
|
|
10480
10917
|
}
|
|
10481
10918
|
const filename = readableFilename(endpoint, body);
|
|
@@ -10487,14 +10924,14 @@ function saveReadable(endpoint, body, response, costUsd) {
|
|
|
10487
10924
|
response
|
|
10488
10925
|
};
|
|
10489
10926
|
try {
|
|
10490
|
-
|
|
10927
|
+
fs7.writeFileSync(path6.join(DATA_DIR, filename), JSON.stringify(entry, null, 2));
|
|
10491
10928
|
} catch {
|
|
10492
10929
|
}
|
|
10493
10930
|
}
|
|
10494
10931
|
function appendCostLog(endpoint, costUsd) {
|
|
10495
10932
|
if (costUsd <= 0) return;
|
|
10496
10933
|
try {
|
|
10497
|
-
|
|
10934
|
+
fs7.mkdirSync(path6.dirname(COST_LOG_FILE2), { recursive: true });
|
|
10498
10935
|
} catch {
|
|
10499
10936
|
}
|
|
10500
10937
|
const entry = {
|
|
@@ -10503,7 +10940,7 @@ function appendCostLog(endpoint, costUsd) {
|
|
|
10503
10940
|
cost_usd: costUsd
|
|
10504
10941
|
};
|
|
10505
10942
|
try {
|
|
10506
|
-
|
|
10943
|
+
fs7.appendFileSync(COST_LOG_FILE2, JSON.stringify(entry) + "\n");
|
|
10507
10944
|
} catch {
|
|
10508
10945
|
}
|
|
10509
10946
|
}
|
|
@@ -10511,7 +10948,7 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
|
|
|
10511
10948
|
const ttl = getTtl(endpoint);
|
|
10512
10949
|
if (ttl > 0) {
|
|
10513
10950
|
try {
|
|
10514
|
-
|
|
10951
|
+
fs7.mkdirSync(CACHE_DIR, { recursive: true });
|
|
10515
10952
|
} catch {
|
|
10516
10953
|
}
|
|
10517
10954
|
const key = cacheKey(endpoint, body);
|
|
@@ -10523,7 +10960,7 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
|
|
|
10523
10960
|
costUsd
|
|
10524
10961
|
};
|
|
10525
10962
|
try {
|
|
10526
|
-
|
|
10963
|
+
fs7.writeFileSync(cachePath(key), JSON.stringify(entry));
|
|
10527
10964
|
} catch {
|
|
10528
10965
|
}
|
|
10529
10966
|
}
|
|
@@ -10531,14 +10968,14 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
|
|
|
10531
10968
|
appendCostLog(endpoint, costUsd);
|
|
10532
10969
|
}
|
|
10533
10970
|
function clearCache() {
|
|
10534
|
-
if (!
|
|
10971
|
+
if (!fs7.existsSync(CACHE_DIR)) return 0;
|
|
10535
10972
|
let count = 0;
|
|
10536
10973
|
try {
|
|
10537
|
-
const files =
|
|
10974
|
+
const files = fs7.readdirSync(CACHE_DIR);
|
|
10538
10975
|
for (const file of files) {
|
|
10539
10976
|
if (file.endsWith(".json")) {
|
|
10540
10977
|
try {
|
|
10541
|
-
|
|
10978
|
+
fs7.unlinkSync(path6.join(CACHE_DIR, file));
|
|
10542
10979
|
count++;
|
|
10543
10980
|
} catch {
|
|
10544
10981
|
}
|
|
@@ -10549,14 +10986,14 @@ function clearCache() {
|
|
|
10549
10986
|
return count;
|
|
10550
10987
|
}
|
|
10551
10988
|
function getCostLogSummary() {
|
|
10552
|
-
if (!
|
|
10989
|
+
if (!fs7.existsSync(COST_LOG_FILE2)) {
|
|
10553
10990
|
return { totalUsd: 0, calls: 0, byEndpoint: {} };
|
|
10554
10991
|
}
|
|
10555
10992
|
let totalUsd = 0;
|
|
10556
10993
|
let calls = 0;
|
|
10557
10994
|
const byEndpoint = {};
|
|
10558
10995
|
try {
|
|
10559
|
-
const content =
|
|
10996
|
+
const content = fs7.readFileSync(COST_LOG_FILE2, "utf-8").trim();
|
|
10560
10997
|
if (!content) return { totalUsd: 0, calls: 0, byEndpoint: {} };
|
|
10561
10998
|
for (const line of content.split("\n")) {
|
|
10562
10999
|
if (!line) continue;
|
|
@@ -10576,9 +11013,9 @@ function getCostLogSummary() {
|
|
|
10576
11013
|
}
|
|
10577
11014
|
|
|
10578
11015
|
// src/setup.ts
|
|
10579
|
-
import { readFileSync as
|
|
11016
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
10580
11017
|
import { homedir as homedir5 } from "os";
|
|
10581
|
-
import { join as
|
|
11018
|
+
import { join as join7 } from "path";
|
|
10582
11019
|
function setupAgentWallet(options) {
|
|
10583
11020
|
const { address, privateKey, isNew } = getOrCreateWallet();
|
|
10584
11021
|
if (isNew) {
|
|
@@ -10627,7 +11064,7 @@ async function setupAgentClient(options = {}) {
|
|
|
10627
11064
|
if (!chain) {
|
|
10628
11065
|
for (const file of ["payment-chain", ".chain"]) {
|
|
10629
11066
|
try {
|
|
10630
|
-
const saved =
|
|
11067
|
+
const saved = readFileSync7(join7(homedir5(), ".blockrun", file), "utf8").trim();
|
|
10631
11068
|
if (saved === "solana" || saved === "base") {
|
|
10632
11069
|
chain = saved;
|
|
10633
11070
|
break;
|