@blockrun/llm 3.16.0 → 3.17.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 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 -->78<!-- /br:models.chatVisible --> models — every request goes to the cheapest model that can handle it,
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
  [![npm](https://img.shields.io/npm/v/@blockrun/llm.svg?style=flat-square)](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 -->78<!-- /br:models.chatVisible -->, one credential** |
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 -->78<!-- /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.
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
@@ -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,81 @@ 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
+ * The Base SDK signs one EIP-3009 authorization and replays it on every poll.
10224
+ * That cannot work here: a Solana payment is a transaction pinned to a recent
10225
+ * blockhash, valid for ~150 blocks (~60s), and a long render outlives it. So
10226
+ * every poll takes a **fresh** 402 and signs again. The gateway binds the job
10227
+ * to the payer address rather than to the signature, which is what makes that
10228
+ * legal — and it settles exactly once, on the poll that returns `completed`,
10229
+ * so signing per poll costs signatures, never money.
10230
+ */
10231
+ async followSolanaJob(submitBody, budgetMs, intervalMs = 2e3) {
10232
+ const id = submitBody.id;
10233
+ const pollPath = submitBody.poll_url;
10234
+ if (!id || !pollPath) return submitBody;
10235
+ const pollUrl = pollPath.startsWith("http") ? pollPath : `${this.apiUrl}${pollPath.startsWith("/") ? "" : "/"}${pollPath}`;
10236
+ const deadline = Date.now() + budgetMs;
10237
+ let lastStatus = submitBody.status || "queued";
10238
+ while (Date.now() < deadline) {
10239
+ await new Promise((r) => setTimeout(r, intervalMs));
10240
+ const challenge = await this.fetchWithTimeout(pollUrl, {
10241
+ method: "GET",
10242
+ headers: { "User-Agent": USER_AGENT }
10243
+ });
10244
+ if (challenge.status === 200) {
10245
+ const done = await challenge.json();
10246
+ return done;
10247
+ }
10248
+ if (challenge.status !== 402) {
10249
+ let errorBody;
10250
+ try {
10251
+ errorBody = await challenge.json();
10252
+ } catch {
10253
+ errorBody = { error: "Poll failed" };
10254
+ }
10255
+ throw new APIError(`Poll failed: ${challenge.status}`, challenge.status, sanitizeErrorResponse(errorBody));
10256
+ }
10257
+ const { paymentPayload, costUsd } = await this.signPaymentFrom402(
10258
+ pollUrl,
10259
+ challenge,
10260
+ true,
10261
+ // always a fresh blockhash; the last one is stale by now
10262
+ pollUrl
10263
+ );
10264
+ const paid = await this.fetchWithTimeout(pollUrl, {
10265
+ method: "GET",
10266
+ headers: { "User-Agent": USER_AGENT, "PAYMENT-SIGNATURE": paymentPayload }
10267
+ });
10268
+ let data = {};
10269
+ try {
10270
+ data = await paid.json();
10271
+ } catch {
10272
+ }
10273
+ lastStatus = data.status || lastStatus;
10274
+ if (lastStatus === "failed") {
10275
+ throw new APIError(
10276
+ `Upstream job failed: ${data.error || "unknown"}`,
10277
+ paid.status,
10278
+ sanitizeErrorResponse(data)
10279
+ );
10280
+ }
10281
+ if (paid.status === 200 && lastStatus === "completed") {
10282
+ this.recordSettlement(costUsd);
10283
+ return data;
10284
+ }
10285
+ if (paid.status !== 200 && paid.status !== 202 && paid.status !== 504) {
10286
+ throw new APIError(`Poll failed: ${paid.status}`, paid.status, sanitizeErrorResponse(data));
10287
+ }
10288
+ }
10289
+ throw new APIError(
10290
+ `Job did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken.`,
10291
+ 504,
10292
+ { id, last_status: lastStatus }
10293
+ );
10294
+ }
10199
10295
  async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
10200
10296
  let paymentHeader = response.headers.get("payment-required");
10201
10297
  if (!paymentHeader) {
@@ -10306,6 +10402,10 @@ var SolanaLLMClient = class {
10306
10402
  body: JSON.stringify(body)
10307
10403
  });
10308
10404
  await this.assertPaid(retryResponse);
10405
+ if (retryResponse.status === 202) {
10406
+ const submitted = await retryResponse.json();
10407
+ return this.followSolanaJob(submitted, this.timeout);
10408
+ }
10309
10409
  this.recordSettlement(costUsd);
10310
10410
  return retryResponse.json();
10311
10411
  }
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,18 @@ 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
+ * The Base SDK signs one EIP-3009 authorization and replays it on every poll.
2925
+ * That cannot work here: a Solana payment is a transaction pinned to a recent
2926
+ * blockhash, valid for ~150 blocks (~60s), and a long render outlives it. So
2927
+ * every poll takes a **fresh** 402 and signs again. The gateway binds the job
2928
+ * to the payer address rather than to the signature, which is what makes that
2929
+ * legal — and it settles exactly once, on the poll that returns `completed`,
2930
+ * so signing per poll costs signatures, never money.
2931
+ */
2932
+ private followSolanaJob;
2900
2933
  private signPaymentFrom402;
2901
2934
  private handlePaymentAndRetry;
2902
2935
  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,18 @@ 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
+ * The Base SDK signs one EIP-3009 authorization and replays it on every poll.
2925
+ * That cannot work here: a Solana payment is a transaction pinned to a recent
2926
+ * blockhash, valid for ~150 blocks (~60s), and a long render outlives it. So
2927
+ * every poll takes a **fresh** 402 and signs again. The gateway binds the job
2928
+ * to the payer address rather than to the signature, which is what makes that
2929
+ * legal — and it settles exactly once, on the poll that returns `completed`,
2930
+ * so signing per poll costs signatures, never money.
2931
+ */
2932
+ private followSolanaJob;
2900
2933
  private signPaymentFrom402;
2901
2934
  private handlePaymentAndRetry;
2902
2935
  private requestWithPaymentRaw;
package/dist/index.js CHANGED
@@ -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,81 @@ 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
+ * The Base SDK signs one EIP-3009 authorization and replays it on every poll.
10103
+ * That cannot work here: a Solana payment is a transaction pinned to a recent
10104
+ * blockhash, valid for ~150 blocks (~60s), and a long render outlives it. So
10105
+ * every poll takes a **fresh** 402 and signs again. The gateway binds the job
10106
+ * to the payer address rather than to the signature, which is what makes that
10107
+ * legal — and it settles exactly once, on the poll that returns `completed`,
10108
+ * so signing per poll costs signatures, never money.
10109
+ */
10110
+ async followSolanaJob(submitBody, budgetMs, intervalMs = 2e3) {
10111
+ const id = submitBody.id;
10112
+ const pollPath = submitBody.poll_url;
10113
+ if (!id || !pollPath) return submitBody;
10114
+ const pollUrl = pollPath.startsWith("http") ? pollPath : `${this.apiUrl}${pollPath.startsWith("/") ? "" : "/"}${pollPath}`;
10115
+ const deadline = Date.now() + budgetMs;
10116
+ let lastStatus = submitBody.status || "queued";
10117
+ while (Date.now() < deadline) {
10118
+ await new Promise((r) => setTimeout(r, intervalMs));
10119
+ const challenge = await this.fetchWithTimeout(pollUrl, {
10120
+ method: "GET",
10121
+ headers: { "User-Agent": USER_AGENT }
10122
+ });
10123
+ if (challenge.status === 200) {
10124
+ const done = await challenge.json();
10125
+ return done;
10126
+ }
10127
+ if (challenge.status !== 402) {
10128
+ let errorBody;
10129
+ try {
10130
+ errorBody = await challenge.json();
10131
+ } catch {
10132
+ errorBody = { error: "Poll failed" };
10133
+ }
10134
+ throw new APIError(`Poll failed: ${challenge.status}`, challenge.status, sanitizeErrorResponse(errorBody));
10135
+ }
10136
+ const { paymentPayload, costUsd } = await this.signPaymentFrom402(
10137
+ pollUrl,
10138
+ challenge,
10139
+ true,
10140
+ // always a fresh blockhash; the last one is stale by now
10141
+ pollUrl
10142
+ );
10143
+ const paid = await this.fetchWithTimeout(pollUrl, {
10144
+ method: "GET",
10145
+ headers: { "User-Agent": USER_AGENT, "PAYMENT-SIGNATURE": paymentPayload }
10146
+ });
10147
+ let data = {};
10148
+ try {
10149
+ data = await paid.json();
10150
+ } catch {
10151
+ }
10152
+ lastStatus = data.status || lastStatus;
10153
+ if (lastStatus === "failed") {
10154
+ throw new APIError(
10155
+ `Upstream job failed: ${data.error || "unknown"}`,
10156
+ paid.status,
10157
+ sanitizeErrorResponse(data)
10158
+ );
10159
+ }
10160
+ if (paid.status === 200 && lastStatus === "completed") {
10161
+ this.recordSettlement(costUsd);
10162
+ return data;
10163
+ }
10164
+ if (paid.status !== 200 && paid.status !== 202 && paid.status !== 504) {
10165
+ throw new APIError(`Poll failed: ${paid.status}`, paid.status, sanitizeErrorResponse(data));
10166
+ }
10167
+ }
10168
+ throw new APIError(
10169
+ `Job did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken.`,
10170
+ 504,
10171
+ { id, last_status: lastStatus }
10172
+ );
10173
+ }
10078
10174
  async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
10079
10175
  let paymentHeader = response.headers.get("payment-required");
10080
10176
  if (!paymentHeader) {
@@ -10185,6 +10281,10 @@ var SolanaLLMClient = class {
10185
10281
  body: JSON.stringify(body)
10186
10282
  });
10187
10283
  await this.assertPaid(retryResponse);
10284
+ if (retryResponse.status === 202) {
10285
+ const submitted = await retryResponse.json();
10286
+ return this.followSolanaJob(submitted, this.timeout);
10287
+ }
10188
10288
  this.recordSettlement(costUsd);
10189
10289
  return retryResponse.json();
10190
10290
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/llm",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
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",