@blockrun/llm 3.16.0 → 3.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.cjs +107 -1
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +107 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
### Cut your LLM bill by <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->%. One line of TypeScript.
|
|
6
6
|
|
|
7
|
-
The smart-routing SDK for <!-- br:models.chatVisible -->
|
|
7
|
+
The smart-routing SDK for <!-- br:models.chatVisible -->79<!-- /br:models.chatVisible --> models — every request goes to the cheapest model that can handle it,
|
|
8
8
|
paid with an API key or per-request USDC on Solana or Base. No vendor lock-in.
|
|
9
9
|
|
|
10
10
|
[](https://www.npmjs.com/package/@blockrun/llm)
|
|
@@ -56,7 +56,7 @@ console.log(r.response); // the proof
|
|
|
56
56
|
| | OpenAI SDK | OpenRouter | LiteLLM | **@blockrun/llm** |
|
|
57
57
|
| ------------------ | -------------- | ----------------- | ---------------- | ----------------------------------------------------------------------- |
|
|
58
58
|
| **Cost routing** | ✗ one vendor | Manual selection | Manual selection | **Automatic — <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% cheaper** |
|
|
59
|
-
| **Models** | GPT only | 200+ | 100+ (BYO keys) | **<!-- br:models.chatVisible -->
|
|
59
|
+
| **Models** | GPT only | 200+ | 100+ (BYO keys) | **<!-- br:models.chatVisible -->79<!-- /br:models.chatVisible -->, one credential** |
|
|
60
60
|
| **Free tier** | ✗ | Rate-limited | ✗ | **<!-- br:models.free -->6<!-- /br:models.free --> models, no signup** |
|
|
61
61
|
| **Auth** | API key | Account + API key | Your API keys | **API key *or* wallet signature** |
|
|
62
62
|
| **Payment** | Card + invoice | Credit card | BYO keys | **Account credit or USDC per-request** |
|
|
@@ -1774,7 +1774,7 @@ The `AnthropicClient` wraps the official `@anthropic-ai/sdk` with a custom fetch
|
|
|
1774
1774
|
## Frequently Asked Questions
|
|
1775
1775
|
|
|
1776
1776
|
### What is @blockrun/llm?
|
|
1777
|
-
@blockrun/llm is a TypeScript SDK that cuts LLM costs by up to <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% with built-in smart routing: every request is routed to the cheapest of <!-- br:models.chatVisible -->
|
|
1777
|
+
@blockrun/llm is a TypeScript SDK that cuts LLM costs by up to <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% with built-in smart routing: every request is routed to the cheapest of <!-- br:models.chatVisible -->79<!-- /br:models.chatVisible --> models (OpenAI, Anthropic, Google, xAI, DeepSeek, Moonshot, and more) that can handle it, then paid per-request in USDC via the x402 protocol — with API key account billing or x402 wallet payments on Solana or Base.
|
|
1778
1778
|
|
|
1779
1779
|
### How does payment work?
|
|
1780
1780
|
When you make an API call, the SDK automatically handles x402 payment. It signs a USDC transaction locally using your wallet private key (which never leaves your machine), and includes the payment proof in the request header. Settlement is non-custodial and instant on Base or Solana.
|
package/dist/index.cjs
CHANGED
|
@@ -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.17.1";
|
|
4664
4664
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
4665
4665
|
|
|
4666
4666
|
// src/client.ts
|
|
@@ -8715,6 +8715,15 @@ var BlockrunClient = class {
|
|
|
8715
8715
|
* deadline exceeded). Settlement happens only when upstream returns 200 +
|
|
8716
8716
|
* completed — upstream failure or caller giving up = no charge.
|
|
8717
8717
|
*
|
|
8718
|
+
* **EVM only, and enforced upstream.** Replaying one signature works because
|
|
8719
|
+
* an EIP-3009 authorization stays valid for as long as it was signed for. A
|
|
8720
|
+
* Solana payment is a transaction pinned to a recent blockhash and expires in
|
|
8721
|
+
* ~150 blocks (~60s), so replaying one could not cover a long render. No
|
|
8722
|
+
* guard is needed here: `signFrom402` already refuses a `solana:` network
|
|
8723
|
+
* outright, so this loop is unreachable on Solana. Use `SolanaLLMClient`,
|
|
8724
|
+
* whose poll loop signs afresh each time; the gateway binds a job to the
|
|
8725
|
+
* payer address rather than to the signature, and still settles exactly once.
|
|
8726
|
+
*
|
|
8718
8727
|
* If the gateway returns 200 directly on submit (no async surface), this
|
|
8719
8728
|
* short-circuits and returns the body. Most long-running endpoints (image,
|
|
8720
8729
|
* video, music, voice) return 202 with a poll_url.
|
|
@@ -9949,6 +9958,18 @@ var SolanaLLMClient = class {
|
|
|
9949
9958
|
}
|
|
9950
9959
|
}
|
|
9951
9960
|
/** Edit an image using img2img (Solana payment). */
|
|
9961
|
+
/**
|
|
9962
|
+
* Generate an image on Solana.
|
|
9963
|
+
*
|
|
9964
|
+
* Slow models answer 202 `{ id, poll_url }`; `requestWithPaymentRaw` follows
|
|
9965
|
+
* that to completion, re-signing per poll, and settles once at the end. Fast
|
|
9966
|
+
* models answer 200 inline and settle there.
|
|
9967
|
+
*/
|
|
9968
|
+
async image(prompt, options = {}) {
|
|
9969
|
+
const body = { prompt, ...options };
|
|
9970
|
+
const data = await this.requestWithPaymentRaw("/v1/images/generations", body);
|
|
9971
|
+
return data;
|
|
9972
|
+
}
|
|
9952
9973
|
async imageEdit(prompt, image, options) {
|
|
9953
9974
|
const body = {
|
|
9954
9975
|
model: options?.model || "openai/gpt-image-2",
|
|
@@ -10196,6 +10217,86 @@ var SolanaLLMClient = class {
|
|
|
10196
10217
|
* @returns the header value to replay with, and what it will settle for.
|
|
10197
10218
|
* @throws PaymentError when the 402 carries no usable Solana requirements.
|
|
10198
10219
|
*/
|
|
10220
|
+
/**
|
|
10221
|
+
* Follow a 202 `{ id, poll_url }` to completion, on Solana.
|
|
10222
|
+
*
|
|
10223
|
+
* Two things differ from Base and both come from the same constraint — a
|
|
10224
|
+
* Solana payment is a transaction pinned to a recent blockhash, valid for
|
|
10225
|
+
* ~150 blocks (~60s), and a long render outlives it:
|
|
10226
|
+
*
|
|
10227
|
+
* 1. **The POST already settled.** Base settles on the completed poll; Solana
|
|
10228
|
+
* cannot, because by then the signed transaction has expired. So sol
|
|
10229
|
+
* settles optimistically at submit, and this loop only fetches the result.
|
|
10230
|
+
* The cost is recorded by the caller at POST, never here — recording it on
|
|
10231
|
+
* completion would lose the charge whenever a paid job then fails.
|
|
10232
|
+
* 2. **Every poll re-signs.** One authorization cannot be replayed across a
|
|
10233
|
+
* long render. The gateway binds the job to the payer address rather than
|
|
10234
|
+
* to the signature, so a fresh signature from the same wallet is accepted
|
|
10235
|
+
* and is never charged again.
|
|
10236
|
+
*/
|
|
10237
|
+
async followSolanaJob(submitBody, budgetMs, intervalMs = 2e3) {
|
|
10238
|
+
const id = submitBody.id;
|
|
10239
|
+
const pollPath = submitBody.poll_url;
|
|
10240
|
+
if (!id || !pollPath) return submitBody;
|
|
10241
|
+
const pollUrl = pollPath.startsWith("http") ? pollPath : `${this.apiUrl}${pollPath.startsWith("/") ? "" : "/"}${pollPath}`;
|
|
10242
|
+
const deadline = Date.now() + budgetMs;
|
|
10243
|
+
let lastStatus = submitBody.status || "queued";
|
|
10244
|
+
while (Date.now() < deadline) {
|
|
10245
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
10246
|
+
const challenge = await this.fetchWithTimeout(pollUrl, {
|
|
10247
|
+
method: "GET",
|
|
10248
|
+
headers: { "User-Agent": USER_AGENT }
|
|
10249
|
+
});
|
|
10250
|
+
if (challenge.status === 200) {
|
|
10251
|
+
const done = await challenge.json();
|
|
10252
|
+
return done;
|
|
10253
|
+
}
|
|
10254
|
+
if (challenge.status !== 402) {
|
|
10255
|
+
let errorBody;
|
|
10256
|
+
try {
|
|
10257
|
+
errorBody = await challenge.json();
|
|
10258
|
+
} catch {
|
|
10259
|
+
errorBody = { error: "Poll failed" };
|
|
10260
|
+
}
|
|
10261
|
+
throw new APIError(`Poll failed: ${challenge.status}`, challenge.status, sanitizeErrorResponse(errorBody));
|
|
10262
|
+
}
|
|
10263
|
+
const { paymentPayload } = await this.signPaymentFrom402(
|
|
10264
|
+
pollUrl,
|
|
10265
|
+
challenge,
|
|
10266
|
+
true,
|
|
10267
|
+
// always a fresh blockhash; the last one is stale by now
|
|
10268
|
+
pollUrl
|
|
10269
|
+
);
|
|
10270
|
+
const paid = await this.fetchWithTimeout(pollUrl, {
|
|
10271
|
+
method: "GET",
|
|
10272
|
+
headers: { "User-Agent": USER_AGENT, "PAYMENT-SIGNATURE": paymentPayload }
|
|
10273
|
+
});
|
|
10274
|
+
let data = {};
|
|
10275
|
+
try {
|
|
10276
|
+
data = await paid.json();
|
|
10277
|
+
} catch {
|
|
10278
|
+
}
|
|
10279
|
+
lastStatus = data.status || lastStatus;
|
|
10280
|
+
if (lastStatus === "failed") {
|
|
10281
|
+
throw new APIError(
|
|
10282
|
+
`Upstream job failed: ${data.error || "unknown"}`,
|
|
10283
|
+
paid.status,
|
|
10284
|
+
sanitizeErrorResponse(data)
|
|
10285
|
+
);
|
|
10286
|
+
}
|
|
10287
|
+
if (paid.status === 200 && lastStatus === "completed") {
|
|
10288
|
+
return data;
|
|
10289
|
+
}
|
|
10290
|
+
if (paid.status !== 200 && paid.status !== 202 && paid.status !== 504) {
|
|
10291
|
+
throw new APIError(`Poll failed: ${paid.status}`, paid.status, sanitizeErrorResponse(data));
|
|
10292
|
+
}
|
|
10293
|
+
}
|
|
10294
|
+
throw new APIError(
|
|
10295
|
+
`Job did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken.`,
|
|
10296
|
+
504,
|
|
10297
|
+
{ id, last_status: lastStatus }
|
|
10298
|
+
);
|
|
10299
|
+
}
|
|
10199
10300
|
async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
|
|
10200
10301
|
let paymentHeader = response.headers.get("payment-required");
|
|
10201
10302
|
if (!paymentHeader) {
|
|
@@ -10306,6 +10407,11 @@ var SolanaLLMClient = class {
|
|
|
10306
10407
|
body: JSON.stringify(body)
|
|
10307
10408
|
});
|
|
10308
10409
|
await this.assertPaid(retryResponse);
|
|
10410
|
+
if (retryResponse.status === 202) {
|
|
10411
|
+
this.recordSettlement(costUsd);
|
|
10412
|
+
const submitted = await retryResponse.json();
|
|
10413
|
+
return this.followSolanaJob(submitted, this.timeout);
|
|
10414
|
+
}
|
|
10309
10415
|
this.recordSettlement(costUsd);
|
|
10310
10416
|
return retryResponse.json();
|
|
10311
10417
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -2264,6 +2264,15 @@ declare class BlockrunClient {
|
|
|
2264
2264
|
* deadline exceeded). Settlement happens only when upstream returns 200 +
|
|
2265
2265
|
* completed — upstream failure or caller giving up = no charge.
|
|
2266
2266
|
*
|
|
2267
|
+
* **EVM only, and enforced upstream.** Replaying one signature works because
|
|
2268
|
+
* an EIP-3009 authorization stays valid for as long as it was signed for. A
|
|
2269
|
+
* Solana payment is a transaction pinned to a recent blockhash and expires in
|
|
2270
|
+
* ~150 blocks (~60s), so replaying one could not cover a long render. No
|
|
2271
|
+
* guard is needed here: `signFrom402` already refuses a `solana:` network
|
|
2272
|
+
* outright, so this loop is unreachable on Solana. Use `SolanaLLMClient`,
|
|
2273
|
+
* whose poll loop signs afresh each time; the gateway binds a job to the
|
|
2274
|
+
* payer address rather than to the signature, and still settles exactly once.
|
|
2275
|
+
*
|
|
2267
2276
|
* If the gateway returns 200 directly on submit (no async surface), this
|
|
2268
2277
|
* short-circuits and returns the body. Most long-running endpoints (image,
|
|
2269
2278
|
* video, music, voice) return 202 with a poll_url.
|
|
@@ -2768,6 +2777,18 @@ declare class SolanaLLMClient {
|
|
|
2768
2777
|
*/
|
|
2769
2778
|
getBalance(): Promise<number>;
|
|
2770
2779
|
/** Edit an image using img2img (Solana payment). */
|
|
2780
|
+
/**
|
|
2781
|
+
* Generate an image on Solana.
|
|
2782
|
+
*
|
|
2783
|
+
* Slow models answer 202 `{ id, poll_url }`; `requestWithPaymentRaw` follows
|
|
2784
|
+
* that to completion, re-signing per poll, and settles once at the end. Fast
|
|
2785
|
+
* models answer 200 inline and settle there.
|
|
2786
|
+
*/
|
|
2787
|
+
image(prompt: string, options?: {
|
|
2788
|
+
model?: string;
|
|
2789
|
+
size?: string;
|
|
2790
|
+
n?: number;
|
|
2791
|
+
} & Record<string, unknown>): Promise<ImageResponse>;
|
|
2771
2792
|
imageEdit(prompt: string, image: string | string[], options?: ImageEditOptions): Promise<ImageResponse>;
|
|
2772
2793
|
/** Standalone search (Solana payment). */
|
|
2773
2794
|
search(query: string, options?: SearchOptions): Promise<SearchResult>;
|
|
@@ -2897,6 +2918,24 @@ declare class SolanaLLMClient {
|
|
|
2897
2918
|
* @returns the header value to replay with, and what it will settle for.
|
|
2898
2919
|
* @throws PaymentError when the 402 carries no usable Solana requirements.
|
|
2899
2920
|
*/
|
|
2921
|
+
/**
|
|
2922
|
+
* Follow a 202 `{ id, poll_url }` to completion, on Solana.
|
|
2923
|
+
*
|
|
2924
|
+
* Two things differ from Base and both come from the same constraint — a
|
|
2925
|
+
* Solana payment is a transaction pinned to a recent blockhash, valid for
|
|
2926
|
+
* ~150 blocks (~60s), and a long render outlives it:
|
|
2927
|
+
*
|
|
2928
|
+
* 1. **The POST already settled.** Base settles on the completed poll; Solana
|
|
2929
|
+
* cannot, because by then the signed transaction has expired. So sol
|
|
2930
|
+
* settles optimistically at submit, and this loop only fetches the result.
|
|
2931
|
+
* The cost is recorded by the caller at POST, never here — recording it on
|
|
2932
|
+
* completion would lose the charge whenever a paid job then fails.
|
|
2933
|
+
* 2. **Every poll re-signs.** One authorization cannot be replayed across a
|
|
2934
|
+
* long render. The gateway binds the job to the payer address rather than
|
|
2935
|
+
* to the signature, so a fresh signature from the same wallet is accepted
|
|
2936
|
+
* and is never charged again.
|
|
2937
|
+
*/
|
|
2938
|
+
private followSolanaJob;
|
|
2900
2939
|
private signPaymentFrom402;
|
|
2901
2940
|
private handlePaymentAndRetry;
|
|
2902
2941
|
private requestWithPaymentRaw;
|
package/dist/index.d.ts
CHANGED
|
@@ -2264,6 +2264,15 @@ declare class BlockrunClient {
|
|
|
2264
2264
|
* deadline exceeded). Settlement happens only when upstream returns 200 +
|
|
2265
2265
|
* completed — upstream failure or caller giving up = no charge.
|
|
2266
2266
|
*
|
|
2267
|
+
* **EVM only, and enforced upstream.** Replaying one signature works because
|
|
2268
|
+
* an EIP-3009 authorization stays valid for as long as it was signed for. A
|
|
2269
|
+
* Solana payment is a transaction pinned to a recent blockhash and expires in
|
|
2270
|
+
* ~150 blocks (~60s), so replaying one could not cover a long render. No
|
|
2271
|
+
* guard is needed here: `signFrom402` already refuses a `solana:` network
|
|
2272
|
+
* outright, so this loop is unreachable on Solana. Use `SolanaLLMClient`,
|
|
2273
|
+
* whose poll loop signs afresh each time; the gateway binds a job to the
|
|
2274
|
+
* payer address rather than to the signature, and still settles exactly once.
|
|
2275
|
+
*
|
|
2267
2276
|
* If the gateway returns 200 directly on submit (no async surface), this
|
|
2268
2277
|
* short-circuits and returns the body. Most long-running endpoints (image,
|
|
2269
2278
|
* video, music, voice) return 202 with a poll_url.
|
|
@@ -2768,6 +2777,18 @@ declare class SolanaLLMClient {
|
|
|
2768
2777
|
*/
|
|
2769
2778
|
getBalance(): Promise<number>;
|
|
2770
2779
|
/** Edit an image using img2img (Solana payment). */
|
|
2780
|
+
/**
|
|
2781
|
+
* Generate an image on Solana.
|
|
2782
|
+
*
|
|
2783
|
+
* Slow models answer 202 `{ id, poll_url }`; `requestWithPaymentRaw` follows
|
|
2784
|
+
* that to completion, re-signing per poll, and settles once at the end. Fast
|
|
2785
|
+
* models answer 200 inline and settle there.
|
|
2786
|
+
*/
|
|
2787
|
+
image(prompt: string, options?: {
|
|
2788
|
+
model?: string;
|
|
2789
|
+
size?: string;
|
|
2790
|
+
n?: number;
|
|
2791
|
+
} & Record<string, unknown>): Promise<ImageResponse>;
|
|
2771
2792
|
imageEdit(prompt: string, image: string | string[], options?: ImageEditOptions): Promise<ImageResponse>;
|
|
2772
2793
|
/** Standalone search (Solana payment). */
|
|
2773
2794
|
search(query: string, options?: SearchOptions): Promise<SearchResult>;
|
|
@@ -2897,6 +2918,24 @@ declare class SolanaLLMClient {
|
|
|
2897
2918
|
* @returns the header value to replay with, and what it will settle for.
|
|
2898
2919
|
* @throws PaymentError when the 402 carries no usable Solana requirements.
|
|
2899
2920
|
*/
|
|
2921
|
+
/**
|
|
2922
|
+
* Follow a 202 `{ id, poll_url }` to completion, on Solana.
|
|
2923
|
+
*
|
|
2924
|
+
* Two things differ from Base and both come from the same constraint — a
|
|
2925
|
+
* Solana payment is a transaction pinned to a recent blockhash, valid for
|
|
2926
|
+
* ~150 blocks (~60s), and a long render outlives it:
|
|
2927
|
+
*
|
|
2928
|
+
* 1. **The POST already settled.** Base settles on the completed poll; Solana
|
|
2929
|
+
* cannot, because by then the signed transaction has expired. So sol
|
|
2930
|
+
* settles optimistically at submit, and this loop only fetches the result.
|
|
2931
|
+
* The cost is recorded by the caller at POST, never here — recording it on
|
|
2932
|
+
* completion would lose the charge whenever a paid job then fails.
|
|
2933
|
+
* 2. **Every poll re-signs.** One authorization cannot be replayed across a
|
|
2934
|
+
* long render. The gateway binds the job to the payer address rather than
|
|
2935
|
+
* to the signature, so a fresh signature from the same wallet is accepted
|
|
2936
|
+
* and is never charged again.
|
|
2937
|
+
*/
|
|
2938
|
+
private followSolanaJob;
|
|
2900
2939
|
private signPaymentFrom402;
|
|
2901
2940
|
private handlePaymentAndRetry;
|
|
2902
2941
|
private requestWithPaymentRaw;
|
package/dist/index.js
CHANGED
|
@@ -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.17.1";
|
|
4543
4543
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
4544
4544
|
|
|
4545
4545
|
// src/client.ts
|
|
@@ -8594,6 +8594,15 @@ var BlockrunClient = class {
|
|
|
8594
8594
|
* deadline exceeded). Settlement happens only when upstream returns 200 +
|
|
8595
8595
|
* completed — upstream failure or caller giving up = no charge.
|
|
8596
8596
|
*
|
|
8597
|
+
* **EVM only, and enforced upstream.** Replaying one signature works because
|
|
8598
|
+
* an EIP-3009 authorization stays valid for as long as it was signed for. A
|
|
8599
|
+
* Solana payment is a transaction pinned to a recent blockhash and expires in
|
|
8600
|
+
* ~150 blocks (~60s), so replaying one could not cover a long render. No
|
|
8601
|
+
* guard is needed here: `signFrom402` already refuses a `solana:` network
|
|
8602
|
+
* outright, so this loop is unreachable on Solana. Use `SolanaLLMClient`,
|
|
8603
|
+
* whose poll loop signs afresh each time; the gateway binds a job to the
|
|
8604
|
+
* payer address rather than to the signature, and still settles exactly once.
|
|
8605
|
+
*
|
|
8597
8606
|
* If the gateway returns 200 directly on submit (no async surface), this
|
|
8598
8607
|
* short-circuits and returns the body. Most long-running endpoints (image,
|
|
8599
8608
|
* video, music, voice) return 202 with a poll_url.
|
|
@@ -9828,6 +9837,18 @@ var SolanaLLMClient = class {
|
|
|
9828
9837
|
}
|
|
9829
9838
|
}
|
|
9830
9839
|
/** Edit an image using img2img (Solana payment). */
|
|
9840
|
+
/**
|
|
9841
|
+
* Generate an image on Solana.
|
|
9842
|
+
*
|
|
9843
|
+
* Slow models answer 202 `{ id, poll_url }`; `requestWithPaymentRaw` follows
|
|
9844
|
+
* that to completion, re-signing per poll, and settles once at the end. Fast
|
|
9845
|
+
* models answer 200 inline and settle there.
|
|
9846
|
+
*/
|
|
9847
|
+
async image(prompt, options = {}) {
|
|
9848
|
+
const body = { prompt, ...options };
|
|
9849
|
+
const data = await this.requestWithPaymentRaw("/v1/images/generations", body);
|
|
9850
|
+
return data;
|
|
9851
|
+
}
|
|
9831
9852
|
async imageEdit(prompt, image, options) {
|
|
9832
9853
|
const body = {
|
|
9833
9854
|
model: options?.model || "openai/gpt-image-2",
|
|
@@ -10075,6 +10096,86 @@ var SolanaLLMClient = class {
|
|
|
10075
10096
|
* @returns the header value to replay with, and what it will settle for.
|
|
10076
10097
|
* @throws PaymentError when the 402 carries no usable Solana requirements.
|
|
10077
10098
|
*/
|
|
10099
|
+
/**
|
|
10100
|
+
* Follow a 202 `{ id, poll_url }` to completion, on Solana.
|
|
10101
|
+
*
|
|
10102
|
+
* Two things differ from Base and both come from the same constraint — a
|
|
10103
|
+
* Solana payment is a transaction pinned to a recent blockhash, valid for
|
|
10104
|
+
* ~150 blocks (~60s), and a long render outlives it:
|
|
10105
|
+
*
|
|
10106
|
+
* 1. **The POST already settled.** Base settles on the completed poll; Solana
|
|
10107
|
+
* cannot, because by then the signed transaction has expired. So sol
|
|
10108
|
+
* settles optimistically at submit, and this loop only fetches the result.
|
|
10109
|
+
* The cost is recorded by the caller at POST, never here — recording it on
|
|
10110
|
+
* completion would lose the charge whenever a paid job then fails.
|
|
10111
|
+
* 2. **Every poll re-signs.** One authorization cannot be replayed across a
|
|
10112
|
+
* long render. The gateway binds the job to the payer address rather than
|
|
10113
|
+
* to the signature, so a fresh signature from the same wallet is accepted
|
|
10114
|
+
* and is never charged again.
|
|
10115
|
+
*/
|
|
10116
|
+
async followSolanaJob(submitBody, budgetMs, intervalMs = 2e3) {
|
|
10117
|
+
const id = submitBody.id;
|
|
10118
|
+
const pollPath = submitBody.poll_url;
|
|
10119
|
+
if (!id || !pollPath) return submitBody;
|
|
10120
|
+
const pollUrl = pollPath.startsWith("http") ? pollPath : `${this.apiUrl}${pollPath.startsWith("/") ? "" : "/"}${pollPath}`;
|
|
10121
|
+
const deadline = Date.now() + budgetMs;
|
|
10122
|
+
let lastStatus = submitBody.status || "queued";
|
|
10123
|
+
while (Date.now() < deadline) {
|
|
10124
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
10125
|
+
const challenge = await this.fetchWithTimeout(pollUrl, {
|
|
10126
|
+
method: "GET",
|
|
10127
|
+
headers: { "User-Agent": USER_AGENT }
|
|
10128
|
+
});
|
|
10129
|
+
if (challenge.status === 200) {
|
|
10130
|
+
const done = await challenge.json();
|
|
10131
|
+
return done;
|
|
10132
|
+
}
|
|
10133
|
+
if (challenge.status !== 402) {
|
|
10134
|
+
let errorBody;
|
|
10135
|
+
try {
|
|
10136
|
+
errorBody = await challenge.json();
|
|
10137
|
+
} catch {
|
|
10138
|
+
errorBody = { error: "Poll failed" };
|
|
10139
|
+
}
|
|
10140
|
+
throw new APIError(`Poll failed: ${challenge.status}`, challenge.status, sanitizeErrorResponse(errorBody));
|
|
10141
|
+
}
|
|
10142
|
+
const { paymentPayload } = await this.signPaymentFrom402(
|
|
10143
|
+
pollUrl,
|
|
10144
|
+
challenge,
|
|
10145
|
+
true,
|
|
10146
|
+
// always a fresh blockhash; the last one is stale by now
|
|
10147
|
+
pollUrl
|
|
10148
|
+
);
|
|
10149
|
+
const paid = await this.fetchWithTimeout(pollUrl, {
|
|
10150
|
+
method: "GET",
|
|
10151
|
+
headers: { "User-Agent": USER_AGENT, "PAYMENT-SIGNATURE": paymentPayload }
|
|
10152
|
+
});
|
|
10153
|
+
let data = {};
|
|
10154
|
+
try {
|
|
10155
|
+
data = await paid.json();
|
|
10156
|
+
} catch {
|
|
10157
|
+
}
|
|
10158
|
+
lastStatus = data.status || lastStatus;
|
|
10159
|
+
if (lastStatus === "failed") {
|
|
10160
|
+
throw new APIError(
|
|
10161
|
+
`Upstream job failed: ${data.error || "unknown"}`,
|
|
10162
|
+
paid.status,
|
|
10163
|
+
sanitizeErrorResponse(data)
|
|
10164
|
+
);
|
|
10165
|
+
}
|
|
10166
|
+
if (paid.status === 200 && lastStatus === "completed") {
|
|
10167
|
+
return data;
|
|
10168
|
+
}
|
|
10169
|
+
if (paid.status !== 200 && paid.status !== 202 && paid.status !== 504) {
|
|
10170
|
+
throw new APIError(`Poll failed: ${paid.status}`, paid.status, sanitizeErrorResponse(data));
|
|
10171
|
+
}
|
|
10172
|
+
}
|
|
10173
|
+
throw new APIError(
|
|
10174
|
+
`Job did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken.`,
|
|
10175
|
+
504,
|
|
10176
|
+
{ id, last_status: lastStatus }
|
|
10177
|
+
);
|
|
10178
|
+
}
|
|
10078
10179
|
async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
|
|
10079
10180
|
let paymentHeader = response.headers.get("payment-required");
|
|
10080
10181
|
if (!paymentHeader) {
|
|
@@ -10185,6 +10286,11 @@ var SolanaLLMClient = class {
|
|
|
10185
10286
|
body: JSON.stringify(body)
|
|
10186
10287
|
});
|
|
10187
10288
|
await this.assertPaid(retryResponse);
|
|
10289
|
+
if (retryResponse.status === 202) {
|
|
10290
|
+
this.recordSettlement(costUsd);
|
|
10291
|
+
const submitted = await retryResponse.json();
|
|
10292
|
+
return this.followSolanaJob(submitted, this.timeout);
|
|
10293
|
+
}
|
|
10188
10294
|
this.recordSettlement(costUsd);
|
|
10189
10295
|
return retryResponse.json();
|
|
10190
10296
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/llm",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.17.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TypeScript SDK for BlockRun - every frontier model behind one OpenAI-compatible client. Pay with a BlockRun API key or per-call in USDC. Smart routing picks the cheapest capable model. No rate limits.",
|
|
6
6
|
"main": "dist/index.cjs",
|