@blockrun/llm 3.17.0 → 3.18.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 -->79<!-- /br:models.chatVisible --> models — every request goes to the cheapest model that can handle it,
7
+ The smart-routing SDK for <!-- br:models.chatVisible -->82<!-- /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 -->79<!-- /br:models.chatVisible -->, one credential** |
59
+ | **Models** | GPT only | 200+ | 100+ (BYO keys) | **<!-- br:models.chatVisible -->82<!-- /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** |
@@ -210,7 +210,6 @@ longer NVIDIA-only**, so pin these by full model id rather than by an
210
210
  | `nvidia/nemotron-3.5-lightning` | 1M | Thinking-mode reasoning at 1M context |
211
211
  | `nvidia/nemotron-3-ultra-550b` | 1M | Largest free model — 550B |
212
212
  | `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` | 256K | Multimodal reasoning — text + images |
213
- | `nvidia/nemotron-3-nano-30b` | 128K | Compact + fast, good for high-volume light tasks |
214
213
  | `nvidia/llama-3.2-11b-vision` | 128K | Vision-language — accepts images |
215
214
  | `cohere/north-mini-code` | 256K | Compact coding model, sub-second responses |
216
215
  | `poolside/laguna-xs-2.1` | 128K | Coding model |
@@ -303,7 +302,7 @@ step — see [How Payment Works](#phase-2--every-request-pays-itself-automatic-x
303
302
  ```typescript
304
303
  // Manually pass a fallback chain to chat() / chatCompletion()
305
304
  const reply = await client.chat('nvidia/nemotron-3.5-lightning', 'hello', {
306
- fallbackModels: ['nvidia/nemotron-3-nano-30b', 'cohere/north-mini-code'],
305
+ fallbackModels: ['nvidia/nemotron-3-ultra-550b', 'cohere/north-mini-code'],
307
306
  });
308
307
  // If nemotron-3.5-lightning times out, the SDK retries against the next model
309
308
  // and logs each hop to stderr: "[@blockrun/llm] <from> -> <to> (...)".
@@ -589,6 +588,18 @@ README is wrong the day after it lands. See **[blockrun.ai/models](https://block
589
588
  for live rates, or read them from the catalog at runtime — `client.listModels()`
590
589
  and `client.listImageModels()` return exactly what the gateway is charging.
591
590
 
591
+ ### OpenAI GPT-6 Family
592
+
593
+ The GPT-6 generation: Astra is the flagship for long-horizon agentic work and
594
+ computer use, Sol the cost-efficient tier for complex coding, Luna the fast
595
+ low-cost tier.
596
+
597
+ | Model | Context |
598
+ |---|---|
599
+ | `openai/gpt-6-astra` | 1.05M |
600
+ | `openai/gpt-6-sol` | 1.05M |
601
+ | `openai/gpt-6-luna` | 1.05M |
602
+
592
603
  ### OpenAI GPT-5.6 Family
593
604
 
594
605
  Three tiers on one 1.05M-context base — Sol (deepest reasoning), Terra
@@ -604,7 +615,7 @@ longer at the same token price.
604
615
  | `openai/gpt-5.6-luna` | 1.05M |
605
616
  | `openai/gpt-5.6-luna-pro` | 1.05M |
606
617
 
607
- ### OpenAI GPT-5.5 / 5.4 / 5.2 Families
618
+ ### OpenAI GPT-5.5 / 5.4 / 5.2 / 5.1 Families
608
619
 
609
620
  | Model | Context | Notes |
610
621
  |---|---|---|
@@ -617,6 +628,7 @@ longer at the same token price.
617
628
  | `openai/gpt-5.4-nano` | 1.05M | |
618
629
  | `openai/gpt-5.2` | 400K | |
619
630
  | `openai/gpt-5.2-pro` | 400K | |
631
+ | `openai/gpt-5.1` | 400K | Configurable reasoning effort |
620
632
  | `openai/gpt-5.3-codex` | 400K | Coding/agentic SKU |
621
633
  | `openai/gpt-5-mini` | 200K | |
622
634
 
@@ -643,11 +655,14 @@ longer at the same token price.
643
655
 
644
656
  | Model | Context | Notes |
645
657
  |---|---|---|
658
+ | `anthropic/claude-fable-5.1` | 1M | Most capable — successor to Fable 5 at the same tier and price |
646
659
  | `anthropic/claude-fable-5` | 1M | Mythos-class flagship above Opus — always-on thinking, 128K output |
660
+ | `anthropic/claude-opus-5.5` | 1M | Newest Opus — Opus-class reasoning at a lower price than Opus 5 |
647
661
  | `anthropic/claude-opus-5` | 1M | Flagship — the baseline the routing savings claim is measured against |
648
662
  | `anthropic/claude-opus-4.8` | 1M | Agentic coding + adaptive thinking, 128K output |
649
663
  | `anthropic/claude-opus-4.7` | 1M | |
650
664
  | `anthropic/claude-opus-4.5` | 200K | |
665
+ | `anthropic/claude-sonnet-5.5` | 1M | Newest Sonnet — everyday coding and agent work, 128K output |
651
666
  | `anthropic/claude-sonnet-5` | 1M | Best cost/quality balance for long-context agent turns |
652
667
  | `anthropic/claude-sonnet-4.6` | 1M | |
653
668
  | `anthropic/claude-sonnet-4.5` | 200K | |
@@ -658,6 +673,7 @@ longer at the same token price.
658
673
  | Model | Context |
659
674
  |---|---|
660
675
  | `google/gemini-3.1-pro` | 1M |
676
+ | `google/gemini-3.8-flash` | 1M |
661
677
  | `google/gemini-3.6-flash` | 1M |
662
678
  | `google/gemini-3.5-flash` | 1M |
663
679
  | `google/gemini-3-flash-preview` | 1M |
@@ -689,7 +705,9 @@ only ranks what `/v1/models` lists.
689
705
 
690
706
  | Model | Context | Notes |
691
707
  |---|---|---|
692
- | `xai/grok-4.5` | 500K | Flagship — reasoning + vision, native Live Search (`search: true`) |
708
+ | `xai/grok-4.7` | 500K | Flagship — reasoning + vision, selectable effort (low → xhigh), native Live Search (`search: true`) |
709
+ | `xai/grok-4.6` | 500K | Reasoning with selectable effort, native Live Search |
710
+ | `xai/grok-4.5` | 500K | Reasoning + vision, native Live Search |
693
711
  | `xai/grok-4.3` | 1M | Reasoning + vision, tuned for agentic workflows |
694
712
  | `xai/grok-build-0.1` | 256K | Fast agentic coding model |
695
713
 
@@ -711,11 +729,10 @@ only ranks what `/v1/models` lists.
711
729
  | `qwen/qwen3.8-flash` | 1M | |
712
730
  | `qwen/qwen3.7-flash` | 1M | Cheapest paid chat model in the catalog |
713
731
 
714
- ### Tencent, Xiaomi
732
+ ### Xiaomi
715
733
 
716
734
  | Model | Context |
717
735
  |---|---|
718
- | `tencent/hy3` | 256K |
719
736
  | `xiaomi/mimo-v2.5` | 1M |
720
737
  | `xiaomi/mimo-v2.5-pro` | 1M |
721
738
 
@@ -729,7 +746,6 @@ Input and output both $0 — no promo, no rate-limit gimmick. The free tier is
729
746
  |---|---|---|
730
747
  | `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` | 256K | Multimodal reasoning — text + images |
731
748
  | `nvidia/nemotron-3.5-lightning` | 1M | Thinking-mode reasoning at 1M context |
732
- | `nvidia/nemotron-3-nano-30b` | 128K | Compact and fast, good for high-volume light tasks |
733
749
  | `nvidia/llama-3.2-11b-vision` | 128K | Vision-language — accepts images |
734
750
  | `nvidia/nemotron-3-ultra-550b` | 1M | Largest free model — 550B, 1M context |
735
751
  | `cohere/north-mini-code` | 256K | Compact coding model, sub-second responses |
@@ -740,11 +756,14 @@ Input and output both $0 — no promo, no rate-limit gimmick. The free tier is
740
756
  |---|---|
741
757
  | `openai/gpt-image-1` | Native GPT-4o image generation |
742
758
  | `openai/gpt-image-2` | Reasoning-driven — multilingual text rendering, character consistency |
759
+ | `openai/gpt-image-2.5-flare` | GPT Image 2.5 |
760
+ | `openai/gpt-image-2.5-sunburst` | GPT Image 2.5 |
743
761
  | `google/nano-banana` | Gemini 2.5 Flash image generation — fast and efficient |
744
762
  | `google/nano-banana-2` | Gemini 3.1 Flash — pro-level quality at Flash speed |
745
763
  | `google/nano-banana-pro` | Gemini 3 Pro — highest quality, up to 4K |
746
764
  | `xai/grok-imagine-image` | Fast, 300 RPM |
747
765
  | `xai/grok-imagine-image-pro` | Quality tier, 30 RPM |
766
+ | `xai/grok-imagine-image-2.0` | Grok Imagine 2.0 |
748
767
  | `bytedance/seedream-5-pro` | Flagship generation + editing, up to 4K-class, reference images |
749
768
  | `zai/cogview-4` | Up to 1440x1440 |
750
769
 
@@ -805,11 +824,40 @@ const r4 = await client.generate('the flower blooms in golden morning light', {
805
824
  lastFrameUrl: 'https://example.com/bloom.jpg',
806
825
  });
807
826
 
808
- // Omni / multi-reference (Seedance 2.0 only): up to 9 reference images
809
- // for character/style consistency. Cite them as "image 1", "image 2" in
810
- // the prompt. Mutually exclusive with imageUrl / lastFrameUrl /
811
- // realFaceAssetId.
812
- const r5 = await client.generate(
827
+ // Seedance output controls. Each is model-gated at the gateway: an
828
+ // unsupported one is a 400 before payment, never silently dropped.
829
+ const r5 = await client.generate('a paper boat drifting down a rain gutter', {
830
+ model: 'bytedance/seedance-2.5',
831
+ bitrateMode: 'high', // Seedance 2.x
832
+ outputFormat: 'mov', // Seedance 2.5 only
833
+ returnLastFrame: true,
834
+ });
835
+ console.log(r5.data[0].last_frame_url); // present when the upstream returns it
836
+ // cameraFixed: true is Seedance 1.5-pro only.
837
+ ```
838
+
839
+ #### Reference images, video and audio (account API key only)
840
+
841
+ Seedance reference media is served by `api.blockrun.ai`, so it needs an
842
+ account API key. The wallet gateways (blockrun.ai, sol.blockrun.ai) refuse
843
+ `referenceImageUrls` / `referenceVideos` / `referenceAudios` with a 400 before
844
+ any payment.
845
+
846
+ | Model | Reference images | Reference video / audio |
847
+ |---|---|---|
848
+ | `bytedance/seedance-2.0` / `-fast` / `-mini` | 1–9 | 1–3 clips of each; audio needs an image or video alongside |
849
+ | `bytedance/seedance-2.5` | 1–30 | — |
850
+
851
+ Reference mode is its own mode: it cannot be mixed with `imageUrl`,
852
+ `lastFrameUrl` or `realFaceAssetId`. Cite images as "image 1", "image 2" and
853
+ clips as "video 1" in the prompt. Reference clips add a per-clip surcharge;
854
+ reference audio must be ≤15.2s.
855
+
856
+ ```ts
857
+ const client = new VideoClient({ apiKey: process.env.BLOCKRUN_API_KEY });
858
+
859
+ // Omni / multi-reference: character/style consistency from images
860
+ const r6 = await client.generate(
813
861
  'the character from image 1 walks through the city from image 2',
814
862
  {
815
863
  model: 'bytedance/seedance-2.0',
@@ -819,6 +867,18 @@ const r5 = await client.generate(
819
867
  ],
820
868
  }
821
869
  );
870
+
871
+ // Reference-to-video: image 1 for the character, video 1 for the motion
872
+ const r7 = await client.generate(
873
+ 'use image 1 for the character and video 1 for the motion',
874
+ {
875
+ model: 'bytedance/seedance-2.0-fast',
876
+ durationSeconds: 5,
877
+ referenceImageUrls: ['https://example.com/character.png'],
878
+ referenceVideos: [{ url: 'https://example.com/motion.mp4' }],
879
+ inputType: 'reference', // optional: 400 if the fields say otherwise
880
+ }
881
+ );
822
882
  ```
823
883
 
824
884
  ### Text-to-Speech & Sound Effects
@@ -1774,7 +1834,7 @@ The `AnthropicClient` wraps the official `@anthropic-ai/sdk` with a custom fetch
1774
1834
  ## Frequently Asked Questions
1775
1835
 
1776
1836
  ### 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 -->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.
1837
+ @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 -->82<!-- /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
1838
 
1779
1839
  ### How does payment work?
1780
1840
  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.16.0";
4663
+ var SDK_VERSION = "3.18.0";
4664
4664
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4665
4665
 
4666
4666
  // src/client.ts
@@ -6958,8 +6958,36 @@ var VideoClient = class {
6958
6958
  "referenceImageUrls is mutually exclusive with imageUrl, lastFrameUrl, and realFaceAssetId."
6959
6959
  );
6960
6960
  }
6961
- if (options.referenceImageUrls.length > 9) {
6962
- throw new Error("referenceImageUrls accepts at most 9 images.");
6961
+ const imageLimit = options.model?.replace(/^bytedance\//, "") === "seedance-2.5" ? 30 : 9;
6962
+ if (options.referenceImageUrls.length > imageLimit) {
6963
+ throw new Error(`referenceImageUrls accepts at most ${imageLimit} images.`);
6964
+ }
6965
+ }
6966
+ for (const [field, clips] of [
6967
+ ["referenceVideos", options?.referenceVideos],
6968
+ ["referenceAudios", options?.referenceAudios]
6969
+ ]) {
6970
+ if (clips === void 0) continue;
6971
+ if (clips.length < 1 || clips.length > 3) {
6972
+ throw new Error(`${field} accepts 1 to 3 clips.`);
6973
+ }
6974
+ for (const clip of clips) {
6975
+ if (typeof clip?.url !== "string" || !/^https?:\/\//i.test(clip.url)) {
6976
+ throw new Error(`${field} URLs must be http(s).`);
6977
+ }
6978
+ if (clip.role !== void 0 && clip.role !== "reference") {
6979
+ throw new Error(`${field} role must be "reference" (or omitted).`);
6980
+ }
6981
+ }
6982
+ }
6983
+ if (options?.referenceVideos?.length || options?.referenceAudios?.length) {
6984
+ if (options.imageUrl || options.lastFrameUrl || options.realFaceAssetId) {
6985
+ throw new Error(
6986
+ "referenceVideos / referenceAudios are mutually exclusive with imageUrl, lastFrameUrl, and realFaceAssetId; combine them with referenceImageUrls instead."
6987
+ );
6988
+ }
6989
+ if (options.referenceAudios?.length && !options.referenceImageUrls?.length && !options.referenceVideos?.length) {
6990
+ throw new Error("referenceAudios requires referenceImageUrls or referenceVideos.");
6963
6991
  }
6964
6992
  }
6965
6993
  const body = {
@@ -6976,7 +7004,14 @@ var VideoClient = class {
6976
7004
  if (options?.generateAudio !== void 0) body.generate_audio = options.generateAudio;
6977
7005
  if (options?.seed !== void 0) body.seed = options.seed;
6978
7006
  if (options?.watermark !== void 0) body.watermark = options.watermark;
6979
- if (options?.returnLastFrame) body.return_last_frame = true;
7007
+ if (options?.returnLastFrame !== void 0) body.return_last_frame = options.returnLastFrame;
7008
+ if (options?.referenceVideos !== void 0) body.reference_videos = options.referenceVideos;
7009
+ if (options?.referenceAudios !== void 0) body.reference_audios = options.referenceAudios;
7010
+ if (options?.bitrateMode !== void 0) body.bitrate_mode = options.bitrateMode;
7011
+ if (options?.outputFormat !== void 0) body.output_format = options.outputFormat;
7012
+ if (options?.cameraFixed !== void 0) body.camera_fixed = options.cameraFixed;
7013
+ if (options?.safetyIdentifier !== void 0) body.safety_identifier = options.safetyIdentifier;
7014
+ if (options?.inputType !== void 0) body.input_type = options.inputType;
6980
7015
  const budgetMs = options?.budgetMs ?? DEFAULT_GENERATE_BUDGET_MS;
6981
7016
  return this.submitAndPoll(body, budgetMs);
6982
7017
  }
@@ -6984,8 +7019,11 @@ var VideoClient = class {
6984
7019
  * Generate a video from a standard Seedance `content[]` body.
6985
7020
  *
6986
7021
  * Targets the gateway's `POST /v1/videos` endpoint, which accepts the
6987
- * mainstream multimodal `content` array (text + a single reference image)
6988
- * used by other Seedance APIs — so callers already holding a
7022
+ * mainstream multimodal `content` array used by other Seedance APIs. Items
7023
+ * may carry a `role` — `first_frame`, `last_frame`, `reference_image`,
7024
+ * `reference_video`, `reference_audio` — which the gateway maps to the same
7025
+ * validated fields as {@link generate}; a role-less single image keeps its
7026
+ * first-frame meaning — so callers already holding a
6989
7027
  * `content[]`-shaped request can submit it unchanged. The gateway validates
6990
7028
  * unsupported inputs *before* charging, then delegates to the same x402
6991
7029
  * submit+poll pipeline as {@link generate}.
@@ -6999,7 +7037,8 @@ var VideoClient = class {
6999
7037
  * `{ type: "image_url", image_url: { url: "https://..." } }`.
7000
7038
  * @param options - `model`, `budgetMs`, plus the same camelCase render options
7001
7039
  * as {@link generate} (`durationSeconds`, `aspectRatio`, `resolution`,
7002
- * `generateAudio`, `seed`, `watermark`, `returnLastFrame`). These are mapped
7040
+ * `generateAudio`, `seed`, `watermark`, `returnLastFrame`, `bitrateMode`,
7041
+ * `outputFormat`, `cameraFixed`, `safetyIdentifier`, `inputType`). These are mapped
7003
7042
  * to the gateway's snake_case fields for you. Any other keys you pass are
7004
7043
  * forwarded verbatim (use snake_case for those, since the gateway reads
7005
7044
  * snake_case only).
@@ -7018,6 +7057,11 @@ var VideoClient = class {
7018
7057
  seed,
7019
7058
  watermark,
7020
7059
  returnLastFrame,
7060
+ bitrateMode,
7061
+ outputFormat,
7062
+ cameraFixed,
7063
+ safetyIdentifier,
7064
+ inputType,
7021
7065
  ...extra
7022
7066
  } = options ?? {};
7023
7067
  const body = { ...extra, content };
@@ -7029,6 +7073,11 @@ var VideoClient = class {
7029
7073
  if (seed !== void 0) body.seed = seed;
7030
7074
  if (watermark !== void 0) body.watermark = watermark;
7031
7075
  if (returnLastFrame !== void 0) body.return_last_frame = returnLastFrame;
7076
+ if (bitrateMode !== void 0) body.bitrate_mode = bitrateMode;
7077
+ if (outputFormat !== void 0) body.output_format = outputFormat;
7078
+ if (cameraFixed !== void 0) body.camera_fixed = cameraFixed;
7079
+ if (safetyIdentifier !== void 0) body.safety_identifier = safetyIdentifier;
7080
+ if (inputType !== void 0) body.input_type = inputType;
7032
7081
  return this.submitAndPoll(body, budgetMs ?? DEFAULT_GENERATE_BUDGET_MS, "/v1/videos");
7033
7082
  }
7034
7083
  // --------------------------------------------------------------------
@@ -10218,15 +10267,24 @@ var SolanaLLMClient = class {
10218
10267
  * @throws PaymentError when the 402 carries no usable Solana requirements.
10219
10268
  */
10220
10269
  /**
10221
- * Follow a 202 `{ id, poll_url }` to completion, on Solana.
10270
+ * Follow a 202 `{ id, poll_url }` to completion, on Solana **image** routes.
10271
+ *
10272
+ * Two things differ from Base and both come from the same constraint — a
10273
+ * Solana payment is a transaction pinned to a recent blockhash, valid for
10274
+ * ~150 blocks (~60s), and a long render outlives it:
10222
10275
  *
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.
10276
+ * 1. **The POST already settled.** Base settles on the completed poll; Solana
10277
+ * cannot, because by then the signed transaction has expired. So sol
10278
+ * settles optimistically at submit, and this loop only fetches the result.
10279
+ * The cost is recorded by the caller at POST, never here — recording it on
10280
+ * completion would lose the charge whenever a paid job then fails.
10281
+ * Solana VIDEO is the exception: it settles on the completed poll and a
10282
+ * failed job is not charged. Do not route video through this loop as-is —
10283
+ * it would book spend for jobs that were never charged.
10284
+ * 2. **Every poll re-signs.** One authorization cannot be replayed across a
10285
+ * long render. The gateway binds the job to the payer address rather than
10286
+ * to the signature, so a fresh signature from the same wallet is accepted
10287
+ * and is never charged again.
10230
10288
  */
10231
10289
  async followSolanaJob(submitBody, budgetMs, intervalMs = 2e3) {
10232
10290
  const id = submitBody.id;
@@ -10254,7 +10312,7 @@ var SolanaLLMClient = class {
10254
10312
  }
10255
10313
  throw new APIError(`Poll failed: ${challenge.status}`, challenge.status, sanitizeErrorResponse(errorBody));
10256
10314
  }
10257
- const { paymentPayload, costUsd } = await this.signPaymentFrom402(
10315
+ const { paymentPayload } = await this.signPaymentFrom402(
10258
10316
  pollUrl,
10259
10317
  challenge,
10260
10318
  true,
@@ -10279,7 +10337,6 @@ var SolanaLLMClient = class {
10279
10337
  );
10280
10338
  }
10281
10339
  if (paid.status === 200 && lastStatus === "completed") {
10282
- this.recordSettlement(costUsd);
10283
10340
  return data;
10284
10341
  }
10285
10342
  if (paid.status !== 200 && paid.status !== 202 && paid.status !== 504) {
@@ -10403,6 +10460,7 @@ var SolanaLLMClient = class {
10403
10460
  });
10404
10461
  await this.assertPaid(retryResponse);
10405
10462
  if (retryResponse.status === 202) {
10463
+ this.recordSettlement(costUsd);
10406
10464
  const submitted = await retryResponse.json();
10407
10465
  return this.followSolanaJob(submitted, this.timeout);
10408
10466
  }
package/dist/index.d.cts CHANGED
@@ -770,6 +770,10 @@ interface VideoClip {
770
770
  request_id?: string;
771
771
  /** True when the gateway mirrored the video to its GCS bucket */
772
772
  backed_up?: boolean;
773
+ /** Final frame image when returnLastFrame is requested and the provider returns it. */
774
+ last_frame_url?: string;
775
+ /** Whether the final frame was mirrored to BlockRun storage. */
776
+ last_frame_backed_up?: boolean;
773
777
  }
774
778
  interface VideoResponse {
775
779
  created: number;
@@ -813,18 +817,55 @@ interface VideoGenerateOptions {
813
817
  * First-and-last-frame interpolation: a second image that seeds the FINAL
814
818
  * frame so the model tweens from `imageUrl` → `lastFrameUrl`. Requires
815
819
  * `imageUrl` (the first frame) and a Seedance model
816
- * (bytedance/seedance-1.5-pro, seedance-2.0, or seedance-2.0-fast).
820
+ * (1.5-pro, 2.0 / Fast / Mini, or 2.5).
817
821
  * Priced identically to image-to-video. Mutually exclusive with
818
822
  * `realFaceAssetId`.
819
823
  */
820
824
  lastFrameUrl?: string;
821
825
  /**
822
- * Omni / multi-reference: up to 9 reference image URLs for character/style
823
- * consistency (**Seedance 2.0 only**). Cite them as "image 1", "image 2"
824
- * in the prompt. Mutually exclusive with `imageUrl` / `lastFrameUrl` /
825
- * `realFaceAssetId`.
826
+ * Omni / multi-reference: up to 9 (Seedance 2.0 / Fast / Mini) or 30
827
+ * (Seedance 2.5) reference image URLs for character/style consistency. Cite
828
+ * them as "image 1", "image 2" in the prompt. Mutually exclusive with
829
+ * `imageUrl` / `lastFrameUrl` / `realFaceAssetId`.
830
+ *
831
+ * **Account API key only.** Reference media is served by `api.blockrun.ai`;
832
+ * the wallet gateways (blockrun.ai, sol.blockrun.ai) refuse it with a 400
833
+ * before any payment.
826
834
  */
827
835
  referenceImageUrls?: string[];
836
+ /**
837
+ * Reference-to-video: 1–3 http(s) motion/style video clips (Seedance 2.0 /
838
+ * Fast / Mini). May be combined with `referenceImageUrls`; mutually exclusive
839
+ * with frame seeds. `role` is optional and, when set, must be `"reference"`.
840
+ * Adds a per-clip surcharge. **Account API key only** (see `referenceImageUrls`).
841
+ */
842
+ referenceVideos?: Array<{
843
+ url: string;
844
+ role?: "reference";
845
+ }>;
846
+ /**
847
+ * Reference audio: 1–3 http(s) clips, each ≤15.2s (Seedance 2.0 / Fast /
848
+ * Mini). Requires `referenceImageUrls` or `referenceVideos` alongside it.
849
+ * Adds a per-clip surcharge. **Account API key only** (see `referenceImageUrls`).
850
+ */
851
+ referenceAudios?: Array<{
852
+ url: string;
853
+ role?: "reference";
854
+ }>;
855
+ /** Output bitrate mode. Seedance 2.0 / Fast / Mini / 2.5 only — other models get a 400. */
856
+ bitrateMode?: "standard" | "high";
857
+ /** Output container. Seedance 2.5 only — other models get a 400. */
858
+ outputFormat?: "mp4" | "mov";
859
+ /** Lock the camera. Seedance 1.5-pro only — other models get a 400. */
860
+ cameraFixed?: boolean;
861
+ /** End-user identifier forwarded for upstream abuse monitoring. Seedance only. */
862
+ safetyIdentifier?: string;
863
+ /**
864
+ * Declare the intended mode. The gateway infers it from the media fields
865
+ * anyway; when this is set and disagrees, it answers 400 (with the inferred
866
+ * mode) before payment instead of rendering the wrong thing.
867
+ */
868
+ inputType?: "text" | "image" | "first_last_frame" | "reference";
828
869
  /** Duration to bill for (defaults to model's default duration) */
829
870
  durationSeconds?: number;
830
871
  /** Output aspect ratio. Token360 / Seedance only — silently ignored by xAI Grok. */
@@ -844,7 +885,10 @@ interface VideoGenerateOptions {
844
885
  seed?: number;
845
886
  /** Embed the upstream watermark on the output. Defaults to false at the gateway. */
846
887
  watermark?: boolean;
847
- /** Return the last frame as an image alongside the clip — useful for chaining. */
888
+ /**
889
+ * Return the last frame as an image alongside the clip — useful for chaining.
890
+ * Read it from `data[0].last_frame_url` (absent if the upstream omits it).
891
+ */
848
892
  returnLastFrame?: boolean;
849
893
  }
850
894
  interface SearchOptions {
@@ -1869,8 +1913,11 @@ declare class VideoClient {
1869
1913
  * Generate a video from a standard Seedance `content[]` body.
1870
1914
  *
1871
1915
  * Targets the gateway's `POST /v1/videos` endpoint, which accepts the
1872
- * mainstream multimodal `content` array (text + a single reference image)
1873
- * used by other Seedance APIs — so callers already holding a
1916
+ * mainstream multimodal `content` array used by other Seedance APIs. Items
1917
+ * may carry a `role` — `first_frame`, `last_frame`, `reference_image`,
1918
+ * `reference_video`, `reference_audio` — which the gateway maps to the same
1919
+ * validated fields as {@link generate}; a role-less single image keeps its
1920
+ * first-frame meaning — so callers already holding a
1874
1921
  * `content[]`-shaped request can submit it unchanged. The gateway validates
1875
1922
  * unsupported inputs *before* charging, then delegates to the same x402
1876
1923
  * submit+poll pipeline as {@link generate}.
@@ -1884,7 +1931,8 @@ declare class VideoClient {
1884
1931
  * `{ type: "image_url", image_url: { url: "https://..." } }`.
1885
1932
  * @param options - `model`, `budgetMs`, plus the same camelCase render options
1886
1933
  * as {@link generate} (`durationSeconds`, `aspectRatio`, `resolution`,
1887
- * `generateAudio`, `seed`, `watermark`, `returnLastFrame`). These are mapped
1934
+ * `generateAudio`, `seed`, `watermark`, `returnLastFrame`, `bitrateMode`,
1935
+ * `outputFormat`, `cameraFixed`, `safetyIdentifier`, `inputType`). These are mapped
1888
1936
  * to the gateway's snake_case fields for you. Any other keys you pass are
1889
1937
  * forwarded verbatim (use snake_case for those, since the gateway reads
1890
1938
  * snake_case only).
@@ -1899,6 +1947,11 @@ declare class VideoClient {
1899
1947
  seed?: number;
1900
1948
  watermark?: boolean;
1901
1949
  returnLastFrame?: boolean;
1950
+ bitrateMode?: "standard" | "high";
1951
+ outputFormat?: "mp4" | "mov";
1952
+ cameraFixed?: boolean;
1953
+ safetyIdentifier?: string;
1954
+ inputType?: "text" | "image" | "first_last_frame" | "reference";
1902
1955
  } & Record<string, unknown>): Promise<VideoResponse>;
1903
1956
  private submitAndPoll;
1904
1957
  /**
@@ -2919,15 +2972,24 @@ declare class SolanaLLMClient {
2919
2972
  * @throws PaymentError when the 402 carries no usable Solana requirements.
2920
2973
  */
2921
2974
  /**
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.
2975
+ * Follow a 202 `{ id, poll_url }` to completion, on Solana **image** routes.
2976
+ *
2977
+ * Two things differ from Base and both come from the same constraint — a
2978
+ * Solana payment is a transaction pinned to a recent blockhash, valid for
2979
+ * ~150 blocks (~60s), and a long render outlives it:
2980
+ *
2981
+ * 1. **The POST already settled.** Base settles on the completed poll; Solana
2982
+ * cannot, because by then the signed transaction has expired. So sol
2983
+ * settles optimistically at submit, and this loop only fetches the result.
2984
+ * The cost is recorded by the caller at POST, never here — recording it on
2985
+ * completion would lose the charge whenever a paid job then fails.
2986
+ * Solana VIDEO is the exception: it settles on the completed poll and a
2987
+ * failed job is not charged. Do not route video through this loop as-is —
2988
+ * it would book spend for jobs that were never charged.
2989
+ * 2. **Every poll re-signs.** One authorization cannot be replayed across a
2990
+ * long render. The gateway binds the job to the payer address rather than
2991
+ * to the signature, so a fresh signature from the same wallet is accepted
2992
+ * and is never charged again.
2931
2993
  */
2932
2994
  private followSolanaJob;
2933
2995
  private signPaymentFrom402;
package/dist/index.d.ts CHANGED
@@ -770,6 +770,10 @@ interface VideoClip {
770
770
  request_id?: string;
771
771
  /** True when the gateway mirrored the video to its GCS bucket */
772
772
  backed_up?: boolean;
773
+ /** Final frame image when returnLastFrame is requested and the provider returns it. */
774
+ last_frame_url?: string;
775
+ /** Whether the final frame was mirrored to BlockRun storage. */
776
+ last_frame_backed_up?: boolean;
773
777
  }
774
778
  interface VideoResponse {
775
779
  created: number;
@@ -813,18 +817,55 @@ interface VideoGenerateOptions {
813
817
  * First-and-last-frame interpolation: a second image that seeds the FINAL
814
818
  * frame so the model tweens from `imageUrl` → `lastFrameUrl`. Requires
815
819
  * `imageUrl` (the first frame) and a Seedance model
816
- * (bytedance/seedance-1.5-pro, seedance-2.0, or seedance-2.0-fast).
820
+ * (1.5-pro, 2.0 / Fast / Mini, or 2.5).
817
821
  * Priced identically to image-to-video. Mutually exclusive with
818
822
  * `realFaceAssetId`.
819
823
  */
820
824
  lastFrameUrl?: string;
821
825
  /**
822
- * Omni / multi-reference: up to 9 reference image URLs for character/style
823
- * consistency (**Seedance 2.0 only**). Cite them as "image 1", "image 2"
824
- * in the prompt. Mutually exclusive with `imageUrl` / `lastFrameUrl` /
825
- * `realFaceAssetId`.
826
+ * Omni / multi-reference: up to 9 (Seedance 2.0 / Fast / Mini) or 30
827
+ * (Seedance 2.5) reference image URLs for character/style consistency. Cite
828
+ * them as "image 1", "image 2" in the prompt. Mutually exclusive with
829
+ * `imageUrl` / `lastFrameUrl` / `realFaceAssetId`.
830
+ *
831
+ * **Account API key only.** Reference media is served by `api.blockrun.ai`;
832
+ * the wallet gateways (blockrun.ai, sol.blockrun.ai) refuse it with a 400
833
+ * before any payment.
826
834
  */
827
835
  referenceImageUrls?: string[];
836
+ /**
837
+ * Reference-to-video: 1–3 http(s) motion/style video clips (Seedance 2.0 /
838
+ * Fast / Mini). May be combined with `referenceImageUrls`; mutually exclusive
839
+ * with frame seeds. `role` is optional and, when set, must be `"reference"`.
840
+ * Adds a per-clip surcharge. **Account API key only** (see `referenceImageUrls`).
841
+ */
842
+ referenceVideos?: Array<{
843
+ url: string;
844
+ role?: "reference";
845
+ }>;
846
+ /**
847
+ * Reference audio: 1–3 http(s) clips, each ≤15.2s (Seedance 2.0 / Fast /
848
+ * Mini). Requires `referenceImageUrls` or `referenceVideos` alongside it.
849
+ * Adds a per-clip surcharge. **Account API key only** (see `referenceImageUrls`).
850
+ */
851
+ referenceAudios?: Array<{
852
+ url: string;
853
+ role?: "reference";
854
+ }>;
855
+ /** Output bitrate mode. Seedance 2.0 / Fast / Mini / 2.5 only — other models get a 400. */
856
+ bitrateMode?: "standard" | "high";
857
+ /** Output container. Seedance 2.5 only — other models get a 400. */
858
+ outputFormat?: "mp4" | "mov";
859
+ /** Lock the camera. Seedance 1.5-pro only — other models get a 400. */
860
+ cameraFixed?: boolean;
861
+ /** End-user identifier forwarded for upstream abuse monitoring. Seedance only. */
862
+ safetyIdentifier?: string;
863
+ /**
864
+ * Declare the intended mode. The gateway infers it from the media fields
865
+ * anyway; when this is set and disagrees, it answers 400 (with the inferred
866
+ * mode) before payment instead of rendering the wrong thing.
867
+ */
868
+ inputType?: "text" | "image" | "first_last_frame" | "reference";
828
869
  /** Duration to bill for (defaults to model's default duration) */
829
870
  durationSeconds?: number;
830
871
  /** Output aspect ratio. Token360 / Seedance only — silently ignored by xAI Grok. */
@@ -844,7 +885,10 @@ interface VideoGenerateOptions {
844
885
  seed?: number;
845
886
  /** Embed the upstream watermark on the output. Defaults to false at the gateway. */
846
887
  watermark?: boolean;
847
- /** Return the last frame as an image alongside the clip — useful for chaining. */
888
+ /**
889
+ * Return the last frame as an image alongside the clip — useful for chaining.
890
+ * Read it from `data[0].last_frame_url` (absent if the upstream omits it).
891
+ */
848
892
  returnLastFrame?: boolean;
849
893
  }
850
894
  interface SearchOptions {
@@ -1869,8 +1913,11 @@ declare class VideoClient {
1869
1913
  * Generate a video from a standard Seedance `content[]` body.
1870
1914
  *
1871
1915
  * Targets the gateway's `POST /v1/videos` endpoint, which accepts the
1872
- * mainstream multimodal `content` array (text + a single reference image)
1873
- * used by other Seedance APIs — so callers already holding a
1916
+ * mainstream multimodal `content` array used by other Seedance APIs. Items
1917
+ * may carry a `role` — `first_frame`, `last_frame`, `reference_image`,
1918
+ * `reference_video`, `reference_audio` — which the gateway maps to the same
1919
+ * validated fields as {@link generate}; a role-less single image keeps its
1920
+ * first-frame meaning — so callers already holding a
1874
1921
  * `content[]`-shaped request can submit it unchanged. The gateway validates
1875
1922
  * unsupported inputs *before* charging, then delegates to the same x402
1876
1923
  * submit+poll pipeline as {@link generate}.
@@ -1884,7 +1931,8 @@ declare class VideoClient {
1884
1931
  * `{ type: "image_url", image_url: { url: "https://..." } }`.
1885
1932
  * @param options - `model`, `budgetMs`, plus the same camelCase render options
1886
1933
  * as {@link generate} (`durationSeconds`, `aspectRatio`, `resolution`,
1887
- * `generateAudio`, `seed`, `watermark`, `returnLastFrame`). These are mapped
1934
+ * `generateAudio`, `seed`, `watermark`, `returnLastFrame`, `bitrateMode`,
1935
+ * `outputFormat`, `cameraFixed`, `safetyIdentifier`, `inputType`). These are mapped
1888
1936
  * to the gateway's snake_case fields for you. Any other keys you pass are
1889
1937
  * forwarded verbatim (use snake_case for those, since the gateway reads
1890
1938
  * snake_case only).
@@ -1899,6 +1947,11 @@ declare class VideoClient {
1899
1947
  seed?: number;
1900
1948
  watermark?: boolean;
1901
1949
  returnLastFrame?: boolean;
1950
+ bitrateMode?: "standard" | "high";
1951
+ outputFormat?: "mp4" | "mov";
1952
+ cameraFixed?: boolean;
1953
+ safetyIdentifier?: string;
1954
+ inputType?: "text" | "image" | "first_last_frame" | "reference";
1902
1955
  } & Record<string, unknown>): Promise<VideoResponse>;
1903
1956
  private submitAndPoll;
1904
1957
  /**
@@ -2919,15 +2972,24 @@ declare class SolanaLLMClient {
2919
2972
  * @throws PaymentError when the 402 carries no usable Solana requirements.
2920
2973
  */
2921
2974
  /**
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.
2975
+ * Follow a 202 `{ id, poll_url }` to completion, on Solana **image** routes.
2976
+ *
2977
+ * Two things differ from Base and both come from the same constraint — a
2978
+ * Solana payment is a transaction pinned to a recent blockhash, valid for
2979
+ * ~150 blocks (~60s), and a long render outlives it:
2980
+ *
2981
+ * 1. **The POST already settled.** Base settles on the completed poll; Solana
2982
+ * cannot, because by then the signed transaction has expired. So sol
2983
+ * settles optimistically at submit, and this loop only fetches the result.
2984
+ * The cost is recorded by the caller at POST, never here — recording it on
2985
+ * completion would lose the charge whenever a paid job then fails.
2986
+ * Solana VIDEO is the exception: it settles on the completed poll and a
2987
+ * failed job is not charged. Do not route video through this loop as-is —
2988
+ * it would book spend for jobs that were never charged.
2989
+ * 2. **Every poll re-signs.** One authorization cannot be replayed across a
2990
+ * long render. The gateway binds the job to the payer address rather than
2991
+ * to the signature, so a fresh signature from the same wallet is accepted
2992
+ * and is never charged again.
2931
2993
  */
2932
2994
  private followSolanaJob;
2933
2995
  private signPaymentFrom402;
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.16.0";
4542
+ var SDK_VERSION = "3.18.0";
4543
4543
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4544
4544
 
4545
4545
  // src/client.ts
@@ -6837,8 +6837,36 @@ var VideoClient = class {
6837
6837
  "referenceImageUrls is mutually exclusive with imageUrl, lastFrameUrl, and realFaceAssetId."
6838
6838
  );
6839
6839
  }
6840
- if (options.referenceImageUrls.length > 9) {
6841
- throw new Error("referenceImageUrls accepts at most 9 images.");
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 = true;
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 (text + a single reference image)
6867
- * used by other Seedance APIs — so callers already holding a
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`). These are mapped
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
  // --------------------------------------------------------------------
@@ -10097,15 +10146,24 @@ var SolanaLLMClient = class {
10097
10146
  * @throws PaymentError when the 402 carries no usable Solana requirements.
10098
10147
  */
10099
10148
  /**
10100
- * Follow a 202 `{ id, poll_url }` to completion, on Solana.
10149
+ * Follow a 202 `{ id, poll_url }` to completion, on Solana **image** routes.
10150
+ *
10151
+ * Two things differ from Base and both come from the same constraint — a
10152
+ * Solana payment is a transaction pinned to a recent blockhash, valid for
10153
+ * ~150 blocks (~60s), and a long render outlives it:
10101
10154
  *
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.
10155
+ * 1. **The POST already settled.** Base settles on the completed poll; Solana
10156
+ * cannot, because by then the signed transaction has expired. So sol
10157
+ * settles optimistically at submit, and this loop only fetches the result.
10158
+ * The cost is recorded by the caller at POST, never here — recording it on
10159
+ * completion would lose the charge whenever a paid job then fails.
10160
+ * Solana VIDEO is the exception: it settles on the completed poll and a
10161
+ * failed job is not charged. Do not route video through this loop as-is —
10162
+ * it would book spend for jobs that were never charged.
10163
+ * 2. **Every poll re-signs.** One authorization cannot be replayed across a
10164
+ * long render. The gateway binds the job to the payer address rather than
10165
+ * to the signature, so a fresh signature from the same wallet is accepted
10166
+ * and is never charged again.
10109
10167
  */
10110
10168
  async followSolanaJob(submitBody, budgetMs, intervalMs = 2e3) {
10111
10169
  const id = submitBody.id;
@@ -10133,7 +10191,7 @@ var SolanaLLMClient = class {
10133
10191
  }
10134
10192
  throw new APIError(`Poll failed: ${challenge.status}`, challenge.status, sanitizeErrorResponse(errorBody));
10135
10193
  }
10136
- const { paymentPayload, costUsd } = await this.signPaymentFrom402(
10194
+ const { paymentPayload } = await this.signPaymentFrom402(
10137
10195
  pollUrl,
10138
10196
  challenge,
10139
10197
  true,
@@ -10158,7 +10216,6 @@ var SolanaLLMClient = class {
10158
10216
  );
10159
10217
  }
10160
10218
  if (paid.status === 200 && lastStatus === "completed") {
10161
- this.recordSettlement(costUsd);
10162
10219
  return data;
10163
10220
  }
10164
10221
  if (paid.status !== 200 && paid.status !== 202 && paid.status !== 504) {
@@ -10282,6 +10339,7 @@ var SolanaLLMClient = class {
10282
10339
  });
10283
10340
  await this.assertPaid(retryResponse);
10284
10341
  if (retryResponse.status === 202) {
10342
+ this.recordSettlement(costUsd);
10285
10343
  const submitted = await retryResponse.json();
10286
10344
  return this.followSolanaJob(submitted, this.timeout);
10287
10345
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/llm",
3
- "version": "3.17.0",
3
+ "version": "3.18.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",