Skip to content

Commit 1dea8b0

Browse files
author
1bcMax
committed
feat: VideoClient + Grok Imagine models + GCS-backed image response fields
- New VideoClient (src/video.ts) for xai/grok-imagine-video at $0.05/sec, 8s default. Blocks until polling completes (~30-120s). - VideoResponse / VideoClip / VideoModel / VideoClientOptions / VideoGenerateOptions types added. - ImageData gains optional source_url + backed_up fields for gateway-mirrored assets. - README documents new Grok Imagine image + video models. - Bumped to 1.7.0.
1 parent 9b89715 commit 1dea8b0

6 files changed

Lines changed: 327 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22

33
All notable changes to @blockrun/llm will be documented in this file.
44

5-
## 1.6.1 (Current)
5+
## 1.7.0
6+
7+
- **New `VideoClient`** — generate AI videos via `xai/grok-imagine-video` ($0.05/sec, 8s default).
8+
- `VideoResponse`, `VideoClip`, `VideoModel`, `VideoClientOptions`, `VideoGenerateOptions` exported.
9+
- Text-to-video and image-to-video supported; client blocks until polling completes (~30-120s).
10+
- `ImageData` gains optional `source_url` and `backed_up` fields for gateway-mirrored assets.
11+
- Grok Imagine image models (`xai/grok-imagine-image`, `-pro`) routable via `ImageClient`.
12+
- Grok 4.20 chat models (`xai/grok-4.20-reasoning`, `-non-reasoning`, `-multi-agent`) routable via the chat API.
13+
14+
## 1.6.1
615

716
- 41+ models with streaming support
817
- Base and Solana chain payments

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,28 @@ All models below have been tested end-to-end via the TypeScript SDK (Feb 2026):
264264
| `google/nano-banana` | $0.05/image |
265265
| `google/nano-banana-pro` | $0.10-0.15/image |
266266
| `black-forest/flux-1.1-pro` | $0.04/image |
267+
| `xai/grok-imagine-image` | $0.02/image |
268+
| `xai/grok-imagine-image-pro` | $0.07/image |
269+
| `zai/cogview-4` | $0.015/image |
270+
271+
### Video Generation
272+
| Model | Price |
273+
|-------|-------|
274+
| `xai/grok-imagine-video` | $0.05/sec (8s default → $0.42/clip) |
275+
276+
```ts
277+
import { VideoClient } from '@blockrun/llm';
278+
279+
const client = new VideoClient();
280+
const result = await client.generate('a red apple slowly spinning on a wooden table');
281+
console.log(result.data[0].url); // permanent MP4 URL
282+
console.log(result.data[0].duration_seconds); // 8
283+
284+
// Image-to-video
285+
const r2 = await client.generate('the subject turns and smiles', {
286+
imageUrl: 'https://example.com/portrait.jpg',
287+
});
288+
```
267289

268290
### Testnet Models (Base Sepolia)
269291
| Model | Price |

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "@blockrun/llm",
3-
"version": "1.6.2",
3+
"version": "1.7.0",
44
"type": "module",
5-
"description": "BlockRun LLM Gateway SDK - Pay-per-request AI via x402 on Base and Solana",
5+
"description": "BlockRun SDK - Pay-per-request AI (LLM, Image, Video, Music) via x402 on Base and Solana",
66
"main": "dist/index.cjs",
77
"module": "dist/index.js",
88
"types": "dist/index.d.ts",

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ export {
6060
type AudioModel,
6161
type MusicClientOptions,
6262
type MusicGenerateOptions,
63+
// Video types
64+
type VideoClip,
65+
type VideoResponse,
66+
type VideoModel,
67+
type VideoClientOptions,
68+
type VideoGenerateOptions,
6369
// Live Search types
6470
type WebSearchSource,
6571
type XSearchSource,

src/types.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ export interface Model {
8080
// Image generation types
8181
export interface ImageData {
8282
url: string;
83+
/** Original upstream URL (e.g. imgen.x.ai). Omitted for data URIs. */
84+
source_url?: string;
85+
/** True when the gateway mirrored the image to its GCS bucket. Omitted for data URIs. */
86+
backed_up?: boolean;
8387
revised_prompt?: string;
8488
b64_json?: string;
8589
}
@@ -380,6 +384,59 @@ export interface MusicGenerateOptions {
380384
lyrics?: string;
381385
}
382386

387+
// Video types
388+
389+
export interface VideoClip {
390+
/** Permanent blockrun-hosted URL (falls back to upstream if backup fails) */
391+
url: string;
392+
/** Original upstream URL (e.g. vidgen.x.ai) */
393+
source_url?: string;
394+
/** Duration of the generated video */
395+
duration_seconds?: number;
396+
/** Upstream provider's request id (xAI) */
397+
request_id?: string;
398+
/** True when the gateway mirrored the video to its GCS bucket */
399+
backed_up?: boolean;
400+
}
401+
402+
export interface VideoResponse {
403+
created: number;
404+
model: string;
405+
data: VideoClip[];
406+
txHash?: string;
407+
}
408+
409+
export interface VideoModel {
410+
id: string;
411+
name: string;
412+
provider: string;
413+
description: string;
414+
pricePerSecond: number;
415+
defaultDurationSeconds: number;
416+
maxDurationSeconds: number;
417+
supportsImageInput: boolean;
418+
available: boolean;
419+
type: "video";
420+
}
421+
422+
export interface VideoClientOptions {
423+
/** EVM wallet private key (hex string starting with 0x) */
424+
privateKey?: `0x${string}` | string;
425+
/** API endpoint URL (default: https://blockrun.ai/api) */
426+
apiUrl?: string;
427+
/** Request timeout in milliseconds (default: 300000 — video gen + polling up to 3 min) */
428+
timeout?: number;
429+
}
430+
431+
export interface VideoGenerateOptions {
432+
/** Model ID (default: "xai/grok-imagine-video") */
433+
model?: "xai/grok-imagine-video" | string;
434+
/** Optional seed image URL for image-to-video */
435+
imageUrl?: string;
436+
/** Duration to bill for (defaults to model's default duration) */
437+
durationSeconds?: number;
438+
}
439+
383440
// Search options for standalone search endpoint
384441
export interface SearchOptions {
385442
/** Source types to search (e.g. ["web", "x", "news"]) */

src/video.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/**
2+
* BlockRun Video Client - Generate short AI videos via x402 micropayments.
3+
*
4+
* SECURITY NOTE - Private Key Handling:
5+
* Your private key NEVER leaves your machine. Here's what happens:
6+
* 1. Key stays local - only used to sign an EIP-712 typed data message
7+
* 2. Only the SIGNATURE is sent in the PAYMENT-SIGNATURE header
8+
* 3. BlockRun verifies the signature on-chain via Coinbase CDP facilitator
9+
*
10+
* Usage:
11+
* import { VideoClient } from '@blockrun/llm';
12+
*
13+
* const client = new VideoClient({ privateKey: '0x...' });
14+
* const result = await client.generate('a red apple slowly spinning on a wooden table');
15+
* console.log(result.data[0].url); // permanent MP4 URL
16+
* console.log(result.data[0].duration_seconds); // 8
17+
*/
18+
19+
import { privateKeyToAccount } from "viem/accounts";
20+
import type { Account } from "viem/accounts";
21+
import {
22+
type VideoClientOptions,
23+
type VideoResponse,
24+
type VideoGenerateOptions,
25+
type Spending,
26+
APIError,
27+
PaymentError,
28+
} from "./types";
29+
import {
30+
createPaymentPayload,
31+
parsePaymentRequired,
32+
extractPaymentDetails,
33+
} from "./x402";
34+
import {
35+
validatePrivateKey,
36+
validateApiUrl,
37+
sanitizeErrorResponse,
38+
} from "./validation";
39+
40+
const DEFAULT_API_URL = "https://blockrun.ai/api";
41+
const DEFAULT_MODEL = "xai/grok-imagine-video";
42+
const DEFAULT_TIMEOUT = 300_000; // video gen + polling up to 3 min
43+
44+
/**
45+
* BlockRun Video Generation Client.
46+
*
47+
* Generates 8-second MP4 clips using xAI's Grok Imagine Video with
48+
* automatic x402 micropayments on Base chain.
49+
*
50+
* Pricing: $0.05/second (default 8s -> $0.42/clip with margin).
51+
* Returned URLs are permanent (mirrored to BlockRun storage).
52+
*/
53+
export class VideoClient {
54+
private account: Account;
55+
private privateKey: `0x${string}`;
56+
private apiUrl: string;
57+
private timeout: number;
58+
private sessionTotalUsd: number = 0;
59+
private sessionCalls: number = 0;
60+
61+
constructor(options: VideoClientOptions = {}) {
62+
const envKey =
63+
typeof process !== "undefined" && process.env
64+
? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY
65+
: undefined;
66+
const privateKey = options.privateKey || envKey;
67+
68+
if (!privateKey) {
69+
throw new Error(
70+
"Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
71+
);
72+
}
73+
74+
validatePrivateKey(privateKey);
75+
this.privateKey = privateKey as `0x${string}`;
76+
this.account = privateKeyToAccount(privateKey as `0x${string}`);
77+
78+
const apiUrl = options.apiUrl || DEFAULT_API_URL;
79+
validateApiUrl(apiUrl);
80+
this.apiUrl = apiUrl.replace(/\/$/, "");
81+
82+
this.timeout = options.timeout || DEFAULT_TIMEOUT;
83+
}
84+
85+
/**
86+
* Generate a short video clip from a text prompt (or text + image).
87+
*
88+
* Blocks until the video is ready (30-120s typical).
89+
*
90+
* @param prompt - Text description of the video
91+
* @param options - Optional generation parameters
92+
* @returns VideoResponse with the clip URL, duration, and upstream request_id
93+
*
94+
* @example Text-to-video
95+
* const result = await client.generate('a hummingbird hovering near a red flower');
96+
* console.log(result.data[0].url);
97+
*
98+
* @example Image-to-video
99+
* const result = await client.generate('the subject turns and smiles', {
100+
* imageUrl: 'https://example.com/portrait.jpg',
101+
* });
102+
*/
103+
async generate(
104+
prompt: string,
105+
options?: VideoGenerateOptions
106+
): Promise<VideoResponse> {
107+
const body: Record<string, unknown> = {
108+
model: options?.model || DEFAULT_MODEL,
109+
prompt,
110+
};
111+
if (options?.imageUrl) body.image_url = options.imageUrl;
112+
if (options?.durationSeconds !== undefined) body.duration_seconds = options.durationSeconds;
113+
114+
return this.requestWithPayment("/v1/videos/generations", body);
115+
}
116+
117+
private async requestWithPayment(
118+
endpoint: string,
119+
body: Record<string, unknown>
120+
): Promise<VideoResponse> {
121+
const url = `${this.apiUrl}${endpoint}`;
122+
123+
const response = await this.fetchWithTimeout(url, {
124+
method: "POST",
125+
headers: { "Content-Type": "application/json" },
126+
body: JSON.stringify(body),
127+
});
128+
129+
if (response.status === 402) {
130+
return this.handlePaymentAndRetry(url, body, response);
131+
}
132+
133+
if (!response.ok) {
134+
let errorBody: unknown;
135+
try { errorBody = await response.json(); } catch { errorBody = { error: "Request failed" }; }
136+
throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
137+
}
138+
139+
return response.json() as Promise<VideoResponse>;
140+
}
141+
142+
private async handlePaymentAndRetry(
143+
url: string,
144+
body: Record<string, unknown>,
145+
response: Response
146+
): Promise<VideoResponse> {
147+
let paymentHeader = response.headers.get("payment-required");
148+
149+
if (!paymentHeader) {
150+
try {
151+
const respBody = (await response.json()) as Record<string, unknown>;
152+
if (respBody.x402 || respBody.accepts) {
153+
paymentHeader = btoa(JSON.stringify(respBody));
154+
}
155+
} catch { /* ignore */ }
156+
}
157+
158+
if (!paymentHeader) {
159+
throw new PaymentError("402 response but no payment requirements found");
160+
}
161+
162+
const paymentRequired = parsePaymentRequired(paymentHeader);
163+
const details = extractPaymentDetails(paymentRequired);
164+
165+
const paymentPayload = await createPaymentPayload(
166+
this.privateKey,
167+
this.account.address,
168+
details.recipient,
169+
details.amount,
170+
details.network || "eip155:8453",
171+
{
172+
resourceUrl: details.resource?.url || `${this.apiUrl}/v1/videos/generations`,
173+
resourceDescription: details.resource?.description || "BlockRun Video Generation",
174+
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
175+
extra: details.extra,
176+
}
177+
);
178+
179+
const retryResponse = await this.fetchWithTimeout(url, {
180+
method: "POST",
181+
headers: {
182+
"Content-Type": "application/json",
183+
"PAYMENT-SIGNATURE": paymentPayload,
184+
},
185+
body: JSON.stringify(body),
186+
});
187+
188+
if (retryResponse.status === 402) {
189+
throw new PaymentError("Payment was rejected. Check your wallet balance.");
190+
}
191+
192+
if (!retryResponse.ok) {
193+
let errorBody: unknown;
194+
try { errorBody = await retryResponse.json(); } catch { errorBody = { error: "Request failed" }; }
195+
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
196+
}
197+
198+
const data = (await retryResponse.json()) as VideoResponse;
199+
200+
// Track spending (best-effort estimate based on default 8s duration)
201+
const billedSeconds = typeof body.duration_seconds === "number" ? body.duration_seconds : 8;
202+
this.sessionCalls++;
203+
this.sessionTotalUsd += 0.05 * billedSeconds * 1.05;
204+
205+
const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
206+
if (txHash) data.txHash = txHash;
207+
208+
return data;
209+
}
210+
211+
private async fetchWithTimeout(url: string, options: RequestInit): Promise<Response> {
212+
const controller = new AbortController();
213+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
214+
try {
215+
return await fetch(url, { ...options, signal: controller.signal });
216+
} finally {
217+
clearTimeout(timeoutId);
218+
}
219+
}
220+
221+
getWalletAddress(): string {
222+
return this.account.address;
223+
}
224+
225+
getSpending(): Spending {
226+
return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
227+
}
228+
}
229+
230+
export default VideoClient;

0 commit comments

Comments
 (0)