Skip to content

Commit 9b89715

Browse files
author
1bcMax
committed
feat: add MusicClient and cogview-4 support
1 parent 556ef8b commit 9b89715

5 files changed

Lines changed: 295 additions & 3 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@blockrun/llm",
3-
"version": "1.6.1",
3+
"version": "1.6.2",
44
"type": "module",
55
"description": "BlockRun LLM Gateway SDK - Pay-per-request AI via x402 on Base and Solana",
66
"main": "dist/index.cjs",

src/image.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,16 @@ import {
4141

4242
const DEFAULT_API_URL = "https://blockrun.ai/api";
4343
const DEFAULT_MODEL = "google/nano-banana";
44+
// Available image models: openai/dall-e-3, openai/gpt-image-1,
45+
// google/nano-banana, google/nano-banana-pro, zai/cogview-4
4446
const DEFAULT_SIZE = "1024x1024";
4547
const DEFAULT_TIMEOUT = 120000; // Images take longer
4648

4749
/**
4850
* BlockRun Image Generation Client.
4951
*
50-
* Generate images using Nano Banana (Google Gemini), DALL-E 3, or GPT Image
51-
* with automatic x402 micropayments on Base chain.
52+
* Generate images using Nano Banana (Google Gemini), DALL-E 3, GPT Image,
53+
* or CogView-4 (Zhipu AI) with automatic x402 micropayments on Base chain.
5254
*/
5355
export class ImageClient {
5456
private account: Account;

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
// Native BlockRun API
3232
export { LLMClient, testnetClient, default } from "./client";
3333
export { ImageClient } from "./image";
34+
export { MusicClient } from "./music";
3435
export {
3536
type ChatMessage,
3637
type ChatChoice,
@@ -53,6 +54,12 @@ export {
5354
type ImageClientOptions,
5455
type ImageGenerateOptions,
5556
type ImageEditOptions,
57+
// Music / Audio types
58+
type AudioTrack,
59+
type MusicResponse,
60+
type AudioModel,
61+
type MusicClientOptions,
62+
type MusicGenerateOptions,
5663
// Live Search types
5764
type WebSearchSource,
5865
type XSearchSource,

src/music.ts

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/**
2+
* BlockRun Music Client - Generate music tracks 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 { MusicClient } from '@blockrun/llm';
12+
*
13+
* const client = new MusicClient({ privateKey: '0x...' });
14+
* const result = await client.generate('upbeat synthwave with neon pads');
15+
* console.log(result.data[0].url); // CDN URL — download within 24h
16+
*/
17+
18+
import { privateKeyToAccount } from "viem/accounts";
19+
import type { Account } from "viem/accounts";
20+
import {
21+
type MusicClientOptions,
22+
type MusicResponse,
23+
type MusicGenerateOptions,
24+
type Spending,
25+
APIError,
26+
PaymentError,
27+
} from "./types";
28+
import {
29+
createPaymentPayload,
30+
parsePaymentRequired,
31+
extractPaymentDetails,
32+
} from "./x402";
33+
import {
34+
validatePrivateKey,
35+
validateApiUrl,
36+
sanitizeErrorResponse,
37+
} from "./validation";
38+
39+
const DEFAULT_API_URL = "https://blockrun.ai/api";
40+
const DEFAULT_MODEL = "minimax/music-2.5+";
41+
const DEFAULT_TIMEOUT = 210_000; // music gen takes 1-3 min
42+
43+
/**
44+
* BlockRun Music Generation Client.
45+
*
46+
* Generate full-length ~3 minute music tracks using MiniMax Music 2.5+
47+
* with automatic x402 micropayments on Base chain.
48+
*
49+
* Pricing: $0.1575/track
50+
* Note: Generated URLs expire in ~24h — download immediately if needed.
51+
*/
52+
export class MusicClient {
53+
private account: Account;
54+
private privateKey: `0x${string}`;
55+
private apiUrl: string;
56+
private timeout: number;
57+
private sessionTotalUsd: number = 0;
58+
private sessionCalls: number = 0;
59+
60+
constructor(options: MusicClientOptions = {}) {
61+
const envKey =
62+
typeof process !== "undefined" && process.env
63+
? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY
64+
: undefined;
65+
const privateKey = options.privateKey || envKey;
66+
67+
if (!privateKey) {
68+
throw new Error(
69+
"Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
70+
);
71+
}
72+
73+
validatePrivateKey(privateKey);
74+
this.privateKey = privateKey as `0x${string}`;
75+
this.account = privateKeyToAccount(privateKey as `0x${string}`);
76+
77+
const apiUrl = options.apiUrl || DEFAULT_API_URL;
78+
validateApiUrl(apiUrl);
79+
this.apiUrl = apiUrl.replace(/\/$/, "");
80+
81+
this.timeout = options.timeout || DEFAULT_TIMEOUT;
82+
}
83+
84+
/**
85+
* Generate a music track from a text prompt.
86+
*
87+
* Takes 1-3 minutes. Returns a CDN URL valid for ~24h.
88+
*
89+
* @param prompt - Music style, mood, or description
90+
* @param options - Optional generation parameters
91+
* @returns MusicResponse with track URL and metadata
92+
*
93+
* @example
94+
* const result = await client.generate('chill lo-fi beats with piano');
95+
* console.log(result.data[0].url); // Download this URL — expires in 24h
96+
*
97+
* @example With lyrics
98+
* const result = await client.generate('upbeat pop song', {
99+
* instrumental: false,
100+
* lyrics: 'Hello world, this is my song...'
101+
* });
102+
*/
103+
async generate(
104+
prompt: string,
105+
options?: MusicGenerateOptions
106+
): Promise<MusicResponse> {
107+
const instrumental = options?.instrumental ?? true;
108+
const lyrics = options?.lyrics?.trim();
109+
110+
if (instrumental && lyrics) {
111+
throw new Error("Cannot specify lyrics when instrumental is true");
112+
}
113+
114+
const body: Record<string, unknown> = {
115+
model: options?.model || DEFAULT_MODEL,
116+
prompt,
117+
instrumental,
118+
};
119+
if (lyrics) body.lyrics = lyrics;
120+
121+
return this.requestWithPayment("/v1/audio/generations", body);
122+
}
123+
124+
private async requestWithPayment(
125+
endpoint: string,
126+
body: Record<string, unknown>
127+
): Promise<MusicResponse> {
128+
const url = `${this.apiUrl}${endpoint}`;
129+
130+
const response = await this.fetchWithTimeout(url, {
131+
method: "POST",
132+
headers: { "Content-Type": "application/json" },
133+
body: JSON.stringify(body),
134+
});
135+
136+
if (response.status === 402) {
137+
return this.handlePaymentAndRetry(url, body, response);
138+
}
139+
140+
if (!response.ok) {
141+
let errorBody: unknown;
142+
try { errorBody = await response.json(); } catch { errorBody = { error: "Request failed" }; }
143+
throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
144+
}
145+
146+
return response.json() as Promise<MusicResponse>;
147+
}
148+
149+
private async handlePaymentAndRetry(
150+
url: string,
151+
body: Record<string, unknown>,
152+
response: Response
153+
): Promise<MusicResponse> {
154+
let paymentHeader = response.headers.get("payment-required");
155+
156+
if (!paymentHeader) {
157+
try {
158+
const respBody = (await response.json()) as Record<string, unknown>;
159+
if (respBody.x402 || respBody.accepts) {
160+
paymentHeader = btoa(JSON.stringify(respBody));
161+
}
162+
} catch { /* ignore */ }
163+
}
164+
165+
if (!paymentHeader) {
166+
throw new PaymentError("402 response but no payment requirements found");
167+
}
168+
169+
const paymentRequired = parsePaymentRequired(paymentHeader);
170+
const details = extractPaymentDetails(paymentRequired);
171+
172+
const paymentPayload = await createPaymentPayload(
173+
this.privateKey,
174+
this.account.address,
175+
details.recipient,
176+
details.amount,
177+
details.network || "eip155:8453",
178+
{
179+
resourceUrl: details.resource?.url || `${this.apiUrl}/v1/audio/generations`,
180+
resourceDescription: details.resource?.description || "BlockRun Music Generation",
181+
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
182+
extra: details.extra,
183+
}
184+
);
185+
186+
const retryResponse = await this.fetchWithTimeout(url, {
187+
method: "POST",
188+
headers: {
189+
"Content-Type": "application/json",
190+
"PAYMENT-SIGNATURE": paymentPayload,
191+
},
192+
body: JSON.stringify(body),
193+
});
194+
195+
if (retryResponse.status === 402) {
196+
throw new PaymentError("Payment was rejected. Check your wallet balance.");
197+
}
198+
199+
if (!retryResponse.ok) {
200+
let errorBody: unknown;
201+
try { errorBody = await retryResponse.json(); } catch { errorBody = { error: "Request failed" }; }
202+
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
203+
}
204+
205+
const data = await retryResponse.json() as MusicResponse;
206+
207+
// Track spending
208+
this.sessionCalls++;
209+
this.sessionTotalUsd += 0.1575;
210+
211+
// Attach tx hash from response header if present
212+
const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
213+
if (txHash) data.txHash = txHash;
214+
215+
return data;
216+
}
217+
218+
private async fetchWithTimeout(url: string, options: RequestInit): Promise<Response> {
219+
const controller = new AbortController();
220+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
221+
try {
222+
return await fetch(url, { ...options, signal: controller.signal });
223+
} finally {
224+
clearTimeout(timeoutId);
225+
}
226+
}
227+
228+
getWalletAddress(): string {
229+
return this.account.address;
230+
}
231+
232+
getSpending(): Spending {
233+
return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
234+
}
235+
}
236+
237+
export default MusicClient;

src/types.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,52 @@ export interface ImageEditOptions {
334334
n?: number;
335335
}
336336

337+
// Music / Audio types
338+
339+
export interface AudioTrack {
340+
url: string;
341+
duration_seconds?: number;
342+
lyrics?: string;
343+
}
344+
345+
export interface MusicResponse {
346+
created: number;
347+
model: string;
348+
data: AudioTrack[];
349+
txHash?: string;
350+
}
351+
352+
export interface AudioModel {
353+
id: string;
354+
name: string;
355+
provider: string;
356+
description: string;
357+
pricePerTrack: number;
358+
maxDurationSeconds: number;
359+
supportsLyrics: boolean;
360+
supportsInstrumental: boolean;
361+
available: boolean;
362+
type: "audio";
363+
}
364+
365+
export interface MusicClientOptions {
366+
/** EVM wallet private key (hex string starting with 0x) */
367+
privateKey?: `0x${string}` | string;
368+
/** API endpoint URL (default: https://blockrun.ai/api) */
369+
apiUrl?: string;
370+
/** Request timeout in milliseconds (default: 210000 — music gen takes 1-3 min) */
371+
timeout?: number;
372+
}
373+
374+
export interface MusicGenerateOptions {
375+
/** Model ID (default: "minimax/music-2.5+") */
376+
model?: "minimax/music-2.5+" | "minimax/music-2.5";
377+
/** Generate without vocals (default: true) */
378+
instrumental?: boolean;
379+
/** Custom lyrics — cannot be used with instrumental: true */
380+
lyrics?: string;
381+
}
382+
337383
// Search options for standalone search endpoint
338384
export interface SearchOptions {
339385
/** Source types to search (e.g. ["web", "x", "news"]) */

0 commit comments

Comments
 (0)