@blockrun/llm 3.2.2 → 3.3.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/dist/index.cjs CHANGED
@@ -2522,8 +2522,9 @@ var DEFAULT_API_URL5 = "https://blockrun.ai/api";
2522
2522
  var DEFAULT_MODEL4 = "xai/grok-imagine-video";
2523
2523
  var DEFAULT_TIMEOUT5 = 12e4;
2524
2524
  var POLL_INTERVAL_MS2 = 5e3;
2525
- var DEFAULT_GENERATE_BUDGET_MS = 3e5;
2525
+ var DEFAULT_GENERATE_BUDGET_MS = 9e5;
2526
2526
  var MAX_TIMEOUT_SECONDS = 600;
2527
+ var MAX_POLL_RESIGNS = 2;
2527
2528
  var VideoClient = class {
2528
2529
  account;
2529
2530
  privateKey;
@@ -2551,8 +2552,9 @@ var VideoClient = class {
2551
2552
  * Generate a short video clip from a text prompt (or text + image).
2552
2553
  *
2553
2554
  * Submits an async job, then polls until the video is ready. Typical total
2554
- * wall-time is 60-180s. If upstream runs past the budget (default 5min),
2555
- * throws without charging.
2555
+ * wall-time is 60-180s, but upstream status can lag several minutes behind
2556
+ * actual completion. If upstream runs past the budget (default 15min),
2557
+ * throws without charging — the job stays claimable ~48h via poll_url.
2556
2558
  *
2557
2559
  * @param prompt - Text description of the video
2558
2560
  * @param options - Optional generation parameters
@@ -2670,21 +2672,9 @@ var VideoClient = class {
2670
2672
  if (resp402.status !== 402) {
2671
2673
  await this.throwApiError(resp402, "Expected 402 on first POST");
2672
2674
  }
2673
- const paymentRequired = await this.extractPaymentRequired(resp402);
2674
- const details = extractPaymentDetails(paymentRequired);
2675
- const paymentPayload = await createPaymentPayload(
2676
- this.privateKey,
2677
- this.account.address,
2678
- details.recipient,
2679
- details.amount,
2680
- details.network || "eip155:8453",
2681
- {
2682
- resourceUrl: details.resource?.url || submitUrl,
2683
- resourceDescription: details.resource?.description || "BlockRun Video Generation",
2684
- // Ensure signed auth covers the entire polling window.
2685
- maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, MAX_TIMEOUT_SECONDS),
2686
- extra: details.extra
2687
- }
2675
+ let { payload: paymentPayload, details } = await this.signFromChallenge(
2676
+ resp402,
2677
+ submitUrl
2688
2678
  );
2689
2679
  const submitResp = await this.fetchWithTimeout(submitUrl, {
2690
2680
  method: "POST",
@@ -2711,6 +2701,7 @@ var VideoClient = class {
2711
2701
  const pollUrl = this.absolute(submitData.poll_url);
2712
2702
  const deadline = Date.now() + budgetMs;
2713
2703
  let lastStatus = submitData.status || "queued";
2704
+ let resignsLeft = MAX_POLL_RESIGNS;
2714
2705
  while (Date.now() < deadline) {
2715
2706
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
2716
2707
  const pollResp = await this.fetchWithTimeout(pollUrl, {
@@ -2733,24 +2724,63 @@ var VideoClient = class {
2733
2724
  sanitizeErrorResponse(pollData)
2734
2725
  );
2735
2726
  }
2736
- if (pollResp.status === 200 && lastStatus === "completed") {
2727
+ if (lastStatus === "completed") {
2737
2728
  const data = pollData;
2738
- const billedSeconds = typeof body.duration_seconds === "number" ? body.duration_seconds : 8;
2739
2729
  this.sessionCalls++;
2740
- this.sessionTotalUsd += 0.05 * billedSeconds * 1.05;
2730
+ this.sessionTotalUsd += Number(details.amount) / 1e6;
2741
2731
  const txHash = pollResp.headers.get("x-payment-receipt") || pollResp.headers.get("X-Payment-Receipt");
2742
2732
  if (txHash) data.txHash = txHash;
2743
2733
  return data;
2744
2734
  }
2735
+ if (pollResp.status === 402) {
2736
+ if (resignsLeft > 0) {
2737
+ resignsLeft--;
2738
+ const challenge = await this.fetchWithTimeout(pollUrl, { method: "GET" });
2739
+ if (challenge.status === 402) {
2740
+ ({ payload: paymentPayload, details } = await this.signFromChallenge(
2741
+ challenge,
2742
+ pollUrl
2743
+ ));
2744
+ continue;
2745
+ }
2746
+ }
2747
+ throw new PaymentError(
2748
+ "Payment verification failed mid-poll (not a signature-expiry). Check the wallet balance and that you poll from the wallet that submitted the job."
2749
+ );
2750
+ }
2745
2751
  if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
2746
2752
  await this.throwApiError(pollResp, "Poll failed");
2747
2753
  }
2748
2754
  }
2749
2755
  throw new APIError(
2750
- `Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken.`,
2756
+ `Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken. The job is NOT lost: it stays claimable for ~48h \u2014 re-GET poll_url with a fresh signature from the same wallet to fetch (and settle) the finished video.`,
2751
2757
  504,
2752
- { id: submitData.id, last_status: lastStatus }
2758
+ { id: submitData.id, last_status: lastStatus, poll_url: pollUrl }
2759
+ );
2760
+ }
2761
+ /**
2762
+ * Parse an x402 challenge response and sign a payment payload for it.
2763
+ * Used for the initial submit AND for mid-poll re-signing after the 600s
2764
+ * authorization window lapses on long polls.
2765
+ */
2766
+ async signFromChallenge(resp402, fallbackUrl) {
2767
+ const paymentRequired = await this.extractPaymentRequired(resp402);
2768
+ const details = extractPaymentDetails(paymentRequired);
2769
+ const payload = await createPaymentPayload(
2770
+ this.privateKey,
2771
+ this.account.address,
2772
+ details.recipient,
2773
+ details.amount,
2774
+ details.network || "eip155:8453",
2775
+ {
2776
+ resourceUrl: details.resource?.url || fallbackUrl,
2777
+ resourceDescription: details.resource?.description || "BlockRun Video Generation",
2778
+ // Ensure signed auth covers as much of the polling window as allowed.
2779
+ maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, MAX_TIMEOUT_SECONDS),
2780
+ extra: details.extra
2781
+ }
2753
2782
  );
2783
+ return { payload, details };
2754
2784
  }
2755
2785
  absolute(url) {
2756
2786
  if (url.startsWith("http://") || url.startsWith("https://")) return url;
package/dist/index.d.cts CHANGED
@@ -1658,7 +1658,8 @@ declare class SpeechClient {
1658
1658
  * POST /v1/videos/generations -> 402 -> sign -> 202 { id, poll_url }
1659
1659
  * GET /v1/videos/generations/{id} -> loop until status=completed
1660
1660
  *
1661
- * The client signs ONCE and replays the same PAYMENT-SIGNATURE on every poll.
1661
+ * The client signs once and replays the same PAYMENT-SIGNATURE on every poll,
1662
+ * re-signing automatically if the 600s authorization window lapses mid-poll.
1662
1663
  * Settlement happens only on the first completed poll, so upstream failure or
1663
1664
  * the caller giving up = zero charge.
1664
1665
  *
@@ -1689,8 +1690,9 @@ declare class VideoClient {
1689
1690
  * Generate a short video clip from a text prompt (or text + image).
1690
1691
  *
1691
1692
  * Submits an async job, then polls until the video is ready. Typical total
1692
- * wall-time is 60-180s. If upstream runs past the budget (default 5min),
1693
- * throws without charging.
1693
+ * wall-time is 60-180s, but upstream status can lag several minutes behind
1694
+ * actual completion. If upstream runs past the budget (default 15min),
1695
+ * throws without charging — the job stays claimable ~48h via poll_url.
1694
1696
  *
1695
1697
  * @param prompt - Text description of the video
1696
1698
  * @param options - Optional generation parameters
@@ -1734,6 +1736,12 @@ declare class VideoClient {
1734
1736
  returnLastFrame?: boolean;
1735
1737
  } & Record<string, unknown>): Promise<VideoResponse>;
1736
1738
  private submitAndPoll;
1739
+ /**
1740
+ * Parse an x402 challenge response and sign a payment payload for it.
1741
+ * Used for the initial submit AND for mid-poll re-signing after the 600s
1742
+ * authorization window lapses on long polls.
1743
+ */
1744
+ private signFromChallenge;
1737
1745
  private absolute;
1738
1746
  private extractPaymentRequired;
1739
1747
  private throwApiError;
package/dist/index.d.ts CHANGED
@@ -1658,7 +1658,8 @@ declare class SpeechClient {
1658
1658
  * POST /v1/videos/generations -> 402 -> sign -> 202 { id, poll_url }
1659
1659
  * GET /v1/videos/generations/{id} -> loop until status=completed
1660
1660
  *
1661
- * The client signs ONCE and replays the same PAYMENT-SIGNATURE on every poll.
1661
+ * The client signs once and replays the same PAYMENT-SIGNATURE on every poll,
1662
+ * re-signing automatically if the 600s authorization window lapses mid-poll.
1662
1663
  * Settlement happens only on the first completed poll, so upstream failure or
1663
1664
  * the caller giving up = zero charge.
1664
1665
  *
@@ -1689,8 +1690,9 @@ declare class VideoClient {
1689
1690
  * Generate a short video clip from a text prompt (or text + image).
1690
1691
  *
1691
1692
  * Submits an async job, then polls until the video is ready. Typical total
1692
- * wall-time is 60-180s. If upstream runs past the budget (default 5min),
1693
- * throws without charging.
1693
+ * wall-time is 60-180s, but upstream status can lag several minutes behind
1694
+ * actual completion. If upstream runs past the budget (default 15min),
1695
+ * throws without charging — the job stays claimable ~48h via poll_url.
1694
1696
  *
1695
1697
  * @param prompt - Text description of the video
1696
1698
  * @param options - Optional generation parameters
@@ -1734,6 +1736,12 @@ declare class VideoClient {
1734
1736
  returnLastFrame?: boolean;
1735
1737
  } & Record<string, unknown>): Promise<VideoResponse>;
1736
1738
  private submitAndPoll;
1739
+ /**
1740
+ * Parse an x402 challenge response and sign a payment payload for it.
1741
+ * Used for the initial submit AND for mid-poll re-signing after the 600s
1742
+ * authorization window lapses on long polls.
1743
+ */
1744
+ private signFromChallenge;
1737
1745
  private absolute;
1738
1746
  private extractPaymentRequired;
1739
1747
  private throwApiError;
package/dist/index.js CHANGED
@@ -2415,8 +2415,9 @@ var DEFAULT_API_URL5 = "https://blockrun.ai/api";
2415
2415
  var DEFAULT_MODEL4 = "xai/grok-imagine-video";
2416
2416
  var DEFAULT_TIMEOUT5 = 12e4;
2417
2417
  var POLL_INTERVAL_MS2 = 5e3;
2418
- var DEFAULT_GENERATE_BUDGET_MS = 3e5;
2418
+ var DEFAULT_GENERATE_BUDGET_MS = 9e5;
2419
2419
  var MAX_TIMEOUT_SECONDS = 600;
2420
+ var MAX_POLL_RESIGNS = 2;
2420
2421
  var VideoClient = class {
2421
2422
  account;
2422
2423
  privateKey;
@@ -2444,8 +2445,9 @@ var VideoClient = class {
2444
2445
  * Generate a short video clip from a text prompt (or text + image).
2445
2446
  *
2446
2447
  * Submits an async job, then polls until the video is ready. Typical total
2447
- * wall-time is 60-180s. If upstream runs past the budget (default 5min),
2448
- * throws without charging.
2448
+ * wall-time is 60-180s, but upstream status can lag several minutes behind
2449
+ * actual completion. If upstream runs past the budget (default 15min),
2450
+ * throws without charging — the job stays claimable ~48h via poll_url.
2449
2451
  *
2450
2452
  * @param prompt - Text description of the video
2451
2453
  * @param options - Optional generation parameters
@@ -2563,21 +2565,9 @@ var VideoClient = class {
2563
2565
  if (resp402.status !== 402) {
2564
2566
  await this.throwApiError(resp402, "Expected 402 on first POST");
2565
2567
  }
2566
- const paymentRequired = await this.extractPaymentRequired(resp402);
2567
- const details = extractPaymentDetails(paymentRequired);
2568
- const paymentPayload = await createPaymentPayload(
2569
- this.privateKey,
2570
- this.account.address,
2571
- details.recipient,
2572
- details.amount,
2573
- details.network || "eip155:8453",
2574
- {
2575
- resourceUrl: details.resource?.url || submitUrl,
2576
- resourceDescription: details.resource?.description || "BlockRun Video Generation",
2577
- // Ensure signed auth covers the entire polling window.
2578
- maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, MAX_TIMEOUT_SECONDS),
2579
- extra: details.extra
2580
- }
2568
+ let { payload: paymentPayload, details } = await this.signFromChallenge(
2569
+ resp402,
2570
+ submitUrl
2581
2571
  );
2582
2572
  const submitResp = await this.fetchWithTimeout(submitUrl, {
2583
2573
  method: "POST",
@@ -2604,6 +2594,7 @@ var VideoClient = class {
2604
2594
  const pollUrl = this.absolute(submitData.poll_url);
2605
2595
  const deadline = Date.now() + budgetMs;
2606
2596
  let lastStatus = submitData.status || "queued";
2597
+ let resignsLeft = MAX_POLL_RESIGNS;
2607
2598
  while (Date.now() < deadline) {
2608
2599
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
2609
2600
  const pollResp = await this.fetchWithTimeout(pollUrl, {
@@ -2626,24 +2617,63 @@ var VideoClient = class {
2626
2617
  sanitizeErrorResponse(pollData)
2627
2618
  );
2628
2619
  }
2629
- if (pollResp.status === 200 && lastStatus === "completed") {
2620
+ if (lastStatus === "completed") {
2630
2621
  const data = pollData;
2631
- const billedSeconds = typeof body.duration_seconds === "number" ? body.duration_seconds : 8;
2632
2622
  this.sessionCalls++;
2633
- this.sessionTotalUsd += 0.05 * billedSeconds * 1.05;
2623
+ this.sessionTotalUsd += Number(details.amount) / 1e6;
2634
2624
  const txHash = pollResp.headers.get("x-payment-receipt") || pollResp.headers.get("X-Payment-Receipt");
2635
2625
  if (txHash) data.txHash = txHash;
2636
2626
  return data;
2637
2627
  }
2628
+ if (pollResp.status === 402) {
2629
+ if (resignsLeft > 0) {
2630
+ resignsLeft--;
2631
+ const challenge = await this.fetchWithTimeout(pollUrl, { method: "GET" });
2632
+ if (challenge.status === 402) {
2633
+ ({ payload: paymentPayload, details } = await this.signFromChallenge(
2634
+ challenge,
2635
+ pollUrl
2636
+ ));
2637
+ continue;
2638
+ }
2639
+ }
2640
+ throw new PaymentError(
2641
+ "Payment verification failed mid-poll (not a signature-expiry). Check the wallet balance and that you poll from the wallet that submitted the job."
2642
+ );
2643
+ }
2638
2644
  if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
2639
2645
  await this.throwApiError(pollResp, "Poll failed");
2640
2646
  }
2641
2647
  }
2642
2648
  throw new APIError(
2643
- `Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken.`,
2649
+ `Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken. The job is NOT lost: it stays claimable for ~48h \u2014 re-GET poll_url with a fresh signature from the same wallet to fetch (and settle) the finished video.`,
2644
2650
  504,
2645
- { id: submitData.id, last_status: lastStatus }
2651
+ { id: submitData.id, last_status: lastStatus, poll_url: pollUrl }
2652
+ );
2653
+ }
2654
+ /**
2655
+ * Parse an x402 challenge response and sign a payment payload for it.
2656
+ * Used for the initial submit AND for mid-poll re-signing after the 600s
2657
+ * authorization window lapses on long polls.
2658
+ */
2659
+ async signFromChallenge(resp402, fallbackUrl) {
2660
+ const paymentRequired = await this.extractPaymentRequired(resp402);
2661
+ const details = extractPaymentDetails(paymentRequired);
2662
+ const payload = await createPaymentPayload(
2663
+ this.privateKey,
2664
+ this.account.address,
2665
+ details.recipient,
2666
+ details.amount,
2667
+ details.network || "eip155:8453",
2668
+ {
2669
+ resourceUrl: details.resource?.url || fallbackUrl,
2670
+ resourceDescription: details.resource?.description || "BlockRun Video Generation",
2671
+ // Ensure signed auth covers as much of the polling window as allowed.
2672
+ maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, MAX_TIMEOUT_SECONDS),
2673
+ extra: details.extra
2674
+ }
2646
2675
  );
2676
+ return { payload, details };
2647
2677
  }
2648
2678
  absolute(url) {
2649
2679
  if (url.startsWith("http://") || url.startsWith("https://")) return url;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/llm",
3
- "version": "3.2.2",
3
+ "version": "3.3.0",
4
4
  "type": "module",
5
5
  "description": "BlockRun SDK - Pay-per-request AI (LLM, Image, Video, Music, Voice) via x402 on Base and Solana",
6
6
  "main": "dist/index.cjs",