@blockrun/llm 0.1.1 → 0.2.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 ADDED
@@ -0,0 +1,1074 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ APIError: () => APIError,
34
+ BASE_CHAIN_ID: () => BASE_CHAIN_ID,
35
+ BlockrunError: () => BlockrunError,
36
+ ImageClient: () => ImageClient,
37
+ LLMClient: () => LLMClient,
38
+ OpenAI: () => OpenAI,
39
+ PaymentError: () => PaymentError,
40
+ USDC_BASE: () => USDC_BASE,
41
+ USDC_BASE_CONTRACT: () => USDC_BASE_CONTRACT,
42
+ WALLET_DIR_PATH: () => WALLET_DIR_PATH,
43
+ WALLET_FILE_PATH: () => WALLET_FILE_PATH,
44
+ createWallet: () => createWallet,
45
+ default: () => client_default,
46
+ formatFundingMessageCompact: () => formatFundingMessageCompact,
47
+ formatNeedsFundingMessage: () => formatNeedsFundingMessage,
48
+ formatWalletCreatedMessage: () => formatWalletCreatedMessage,
49
+ getEip681Uri: () => getEip681Uri,
50
+ getOrCreateWallet: () => getOrCreateWallet,
51
+ getPaymentLinks: () => getPaymentLinks,
52
+ getWalletAddress: () => getWalletAddress,
53
+ loadWallet: () => loadWallet,
54
+ saveWallet: () => saveWallet
55
+ });
56
+ module.exports = __toCommonJS(index_exports);
57
+
58
+ // src/client.ts
59
+ var import_accounts2 = require("viem/accounts");
60
+
61
+ // src/types.ts
62
+ var BlockrunError = class extends Error {
63
+ constructor(message) {
64
+ super(message);
65
+ this.name = "BlockrunError";
66
+ }
67
+ };
68
+ var PaymentError = class extends BlockrunError {
69
+ constructor(message) {
70
+ super(message);
71
+ this.name = "PaymentError";
72
+ }
73
+ };
74
+ var APIError = class extends BlockrunError {
75
+ statusCode;
76
+ response;
77
+ constructor(message, statusCode, response) {
78
+ super(message);
79
+ this.name = "APIError";
80
+ this.statusCode = statusCode;
81
+ this.response = response;
82
+ }
83
+ };
84
+
85
+ // src/x402.ts
86
+ var import_accounts = require("viem/accounts");
87
+ var BASE_CHAIN_ID = 8453;
88
+ var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
89
+ var USDC_DOMAIN = {
90
+ name: "USD Coin",
91
+ version: "2",
92
+ chainId: BASE_CHAIN_ID,
93
+ verifyingContract: USDC_BASE
94
+ };
95
+ var TRANSFER_TYPES = {
96
+ TransferWithAuthorization: [
97
+ { name: "from", type: "address" },
98
+ { name: "to", type: "address" },
99
+ { name: "value", type: "uint256" },
100
+ { name: "validAfter", type: "uint256" },
101
+ { name: "validBefore", type: "uint256" },
102
+ { name: "nonce", type: "bytes32" }
103
+ ]
104
+ };
105
+ function createNonce() {
106
+ const bytes = new Uint8Array(32);
107
+ crypto.getRandomValues(bytes);
108
+ return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
109
+ }
110
+ async function createPaymentPayload(privateKey, fromAddress, recipient, amount, network = "eip155:8453", options = {}) {
111
+ const now = Math.floor(Date.now() / 1e3);
112
+ const validAfter = now - 600;
113
+ const validBefore = now + (options.maxTimeoutSeconds || 300);
114
+ const nonce = createNonce();
115
+ const domain = USDC_DOMAIN;
116
+ const signature = await (0, import_accounts.signTypedData)({
117
+ privateKey,
118
+ domain,
119
+ types: TRANSFER_TYPES,
120
+ primaryType: "TransferWithAuthorization",
121
+ message: {
122
+ from: fromAddress,
123
+ to: recipient,
124
+ value: BigInt(amount),
125
+ validAfter: BigInt(validAfter),
126
+ validBefore: BigInt(validBefore),
127
+ nonce
128
+ }
129
+ });
130
+ const paymentData = {
131
+ x402Version: 2,
132
+ resource: {
133
+ url: options.resourceUrl || "https://blockrun.ai/api/v1/chat/completions",
134
+ description: options.resourceDescription || "BlockRun AI API call",
135
+ mimeType: "application/json"
136
+ },
137
+ accepted: {
138
+ scheme: "exact",
139
+ network,
140
+ amount,
141
+ asset: USDC_BASE,
142
+ payTo: recipient,
143
+ maxTimeoutSeconds: options.maxTimeoutSeconds || 300,
144
+ extra: { name: "USD Coin", version: "2" }
145
+ },
146
+ payload: {
147
+ signature,
148
+ authorization: {
149
+ from: fromAddress,
150
+ to: recipient,
151
+ value: amount,
152
+ validAfter: validAfter.toString(),
153
+ validBefore: validBefore.toString(),
154
+ nonce
155
+ }
156
+ },
157
+ extensions: options.extensions || {}
158
+ };
159
+ return btoa(JSON.stringify(paymentData));
160
+ }
161
+ function parsePaymentRequired(headerValue) {
162
+ try {
163
+ const decoded = atob(headerValue);
164
+ const parsed = JSON.parse(decoded);
165
+ if (!parsed.accepts || !Array.isArray(parsed.accepts)) {
166
+ throw new Error("Invalid payment required structure: missing or invalid 'accepts' field");
167
+ }
168
+ return parsed;
169
+ } catch (error) {
170
+ if (error instanceof Error) {
171
+ if (error.message.includes("Invalid payment required structure")) {
172
+ throw error;
173
+ }
174
+ throw new Error("Failed to parse payment required header: invalid format");
175
+ }
176
+ throw new Error("Failed to parse payment required header");
177
+ }
178
+ }
179
+ function extractPaymentDetails(paymentRequired, preferredNetwork) {
180
+ const accepts = paymentRequired.accepts || [];
181
+ if (accepts.length === 0) {
182
+ throw new Error("No payment options in payment required response");
183
+ }
184
+ let option = null;
185
+ if (preferredNetwork) {
186
+ option = accepts.find((opt) => opt.network === preferredNetwork) || null;
187
+ }
188
+ if (!option) {
189
+ option = accepts[0];
190
+ }
191
+ const amount = option.amount || option.maxAmountRequired;
192
+ if (!amount) {
193
+ throw new Error("No amount found in payment requirements");
194
+ }
195
+ return {
196
+ amount,
197
+ recipient: option.payTo,
198
+ network: option.network,
199
+ asset: option.asset,
200
+ scheme: option.scheme,
201
+ maxTimeoutSeconds: option.maxTimeoutSeconds || 300,
202
+ extra: option.extra,
203
+ resource: paymentRequired.resource
204
+ };
205
+ }
206
+
207
+ // src/validation.ts
208
+ var LOCALHOST_DOMAINS = ["localhost", "127.0.0.1"];
209
+ function validatePrivateKey(key) {
210
+ if (typeof key !== "string") {
211
+ throw new Error("Private key must be a string");
212
+ }
213
+ if (!key.startsWith("0x")) {
214
+ throw new Error("Private key must start with 0x");
215
+ }
216
+ if (key.length !== 66) {
217
+ throw new Error(
218
+ "Private key must be 66 characters (0x + 64 hexadecimal characters)"
219
+ );
220
+ }
221
+ if (!/^0x[0-9a-fA-F]{64}$/.test(key)) {
222
+ throw new Error(
223
+ "Private key must contain only hexadecimal characters (0-9, a-f, A-F)"
224
+ );
225
+ }
226
+ }
227
+ function validateApiUrl(url) {
228
+ let parsed;
229
+ try {
230
+ parsed = new URL(url);
231
+ } catch {
232
+ throw new Error("Invalid API URL format");
233
+ }
234
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
235
+ throw new Error(
236
+ `Invalid protocol: ${parsed.protocol}. Use http:// or https://`
237
+ );
238
+ }
239
+ const isLocalhost = LOCALHOST_DOMAINS.includes(parsed.hostname);
240
+ if (parsed.protocol !== "https:" && !isLocalhost) {
241
+ throw new Error(
242
+ `API URL must use HTTPS for non-localhost endpoints. Use https:// instead of ${parsed.protocol}//`
243
+ );
244
+ }
245
+ }
246
+ function sanitizeErrorResponse(errorBody) {
247
+ if (typeof errorBody !== "object" || errorBody === null) {
248
+ return { message: "API request failed" };
249
+ }
250
+ const body = errorBody;
251
+ return {
252
+ message: typeof body.error === "string" ? body.error : "API request failed",
253
+ code: typeof body.code === "string" ? body.code : void 0
254
+ };
255
+ }
256
+ function validateResourceUrl(url, baseUrl) {
257
+ try {
258
+ const parsed = new URL(url);
259
+ const baseParsed = new URL(baseUrl);
260
+ if (parsed.hostname !== baseParsed.hostname) {
261
+ console.warn(
262
+ `Resource URL hostname mismatch: ${parsed.hostname} vs ${baseParsed.hostname}. Using safe default instead.`
263
+ );
264
+ return `${baseUrl}/v1/chat/completions`;
265
+ }
266
+ if (parsed.protocol !== baseParsed.protocol) {
267
+ console.warn(
268
+ `Resource URL protocol mismatch: ${parsed.protocol} vs ${baseParsed.protocol}. Using safe default instead.`
269
+ );
270
+ return `${baseUrl}/v1/chat/completions`;
271
+ }
272
+ return url;
273
+ } catch {
274
+ console.warn(`Invalid resource URL format: ${url}. Using safe default.`);
275
+ return `${baseUrl}/v1/chat/completions`;
276
+ }
277
+ }
278
+
279
+ // src/client.ts
280
+ var DEFAULT_API_URL = "https://blockrun.ai/api";
281
+ var DEFAULT_MAX_TOKENS = 1024;
282
+ var DEFAULT_TIMEOUT = 6e4;
283
+ var LLMClient = class {
284
+ account;
285
+ privateKey;
286
+ apiUrl;
287
+ timeout;
288
+ sessionTotalUsd = 0;
289
+ sessionCalls = 0;
290
+ /**
291
+ * Initialize the BlockRun LLM client.
292
+ *
293
+ * @param options - Client configuration options (optional if BASE_CHAIN_WALLET_KEY env var is set)
294
+ */
295
+ constructor(options = {}) {
296
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BASE_CHAIN_WALLET_KEY : void 0;
297
+ const privateKey = options.privateKey || envKey;
298
+ if (!privateKey) {
299
+ throw new Error(
300
+ "Private key required. Pass privateKey in options or set BASE_CHAIN_WALLET_KEY environment variable."
301
+ );
302
+ }
303
+ validatePrivateKey(privateKey);
304
+ this.privateKey = privateKey;
305
+ this.account = (0, import_accounts2.privateKeyToAccount)(privateKey);
306
+ const apiUrl = options.apiUrl || DEFAULT_API_URL;
307
+ validateApiUrl(apiUrl);
308
+ this.apiUrl = apiUrl.replace(/\/$/, "");
309
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
310
+ }
311
+ /**
312
+ * Simple 1-line chat interface.
313
+ *
314
+ * @param model - Model ID (e.g., 'openai/gpt-4o', 'anthropic/claude-sonnet-4')
315
+ * @param prompt - User message
316
+ * @param options - Optional chat parameters
317
+ * @returns Assistant's response text
318
+ *
319
+ * @example
320
+ * const response = await client.chat('gpt-4o', 'What is the capital of France?');
321
+ * console.log(response); // 'The capital of France is Paris.'
322
+ */
323
+ async chat(model, prompt, options) {
324
+ const messages = [];
325
+ if (options?.system) {
326
+ messages.push({ role: "system", content: options.system });
327
+ }
328
+ messages.push({ role: "user", content: prompt });
329
+ const result = await this.chatCompletion(model, messages, {
330
+ maxTokens: options?.maxTokens,
331
+ temperature: options?.temperature,
332
+ topP: options?.topP,
333
+ search: options?.search,
334
+ searchParameters: options?.searchParameters
335
+ });
336
+ return result.choices[0].message.content;
337
+ }
338
+ /**
339
+ * Full chat completion interface (OpenAI-compatible).
340
+ *
341
+ * @param model - Model ID
342
+ * @param messages - Array of messages with role and content
343
+ * @param options - Optional completion parameters
344
+ * @returns ChatResponse object with choices and usage
345
+ */
346
+ async chatCompletion(model, messages, options) {
347
+ const body = {
348
+ model,
349
+ messages,
350
+ max_tokens: options?.maxTokens || DEFAULT_MAX_TOKENS
351
+ };
352
+ if (options?.temperature !== void 0) {
353
+ body.temperature = options.temperature;
354
+ }
355
+ if (options?.topP !== void 0) {
356
+ body.top_p = options.topP;
357
+ }
358
+ if (options?.searchParameters !== void 0) {
359
+ body.search_parameters = options.searchParameters;
360
+ } else if (options?.search === true) {
361
+ body.search_parameters = { mode: "on" };
362
+ }
363
+ return this.requestWithPayment("/v1/chat/completions", body);
364
+ }
365
+ /**
366
+ * Make a request with automatic x402 payment handling.
367
+ */
368
+ async requestWithPayment(endpoint, body) {
369
+ const url = `${this.apiUrl}${endpoint}`;
370
+ const response = await this.fetchWithTimeout(url, {
371
+ method: "POST",
372
+ headers: { "Content-Type": "application/json" },
373
+ body: JSON.stringify(body)
374
+ });
375
+ if (response.status === 402) {
376
+ return this.handlePaymentAndRetry(url, body, response);
377
+ }
378
+ if (!response.ok) {
379
+ let errorBody;
380
+ try {
381
+ errorBody = await response.json();
382
+ } catch {
383
+ errorBody = { error: "Request failed" };
384
+ }
385
+ throw new APIError(
386
+ `API error: ${response.status}`,
387
+ response.status,
388
+ sanitizeErrorResponse(errorBody)
389
+ );
390
+ }
391
+ return response.json();
392
+ }
393
+ /**
394
+ * Handle 402 response: parse requirements, sign payment, retry.
395
+ */
396
+ async handlePaymentAndRetry(url, body, response) {
397
+ let paymentHeader = response.headers.get("payment-required");
398
+ if (!paymentHeader) {
399
+ try {
400
+ const respBody = await response.json();
401
+ if (respBody.x402 || respBody.accepts) {
402
+ paymentHeader = btoa(JSON.stringify(respBody));
403
+ }
404
+ } catch (parseError) {
405
+ console.debug("Failed to parse payment header from response body", parseError);
406
+ }
407
+ }
408
+ if (!paymentHeader) {
409
+ throw new PaymentError("402 response but no payment requirements found");
410
+ }
411
+ const paymentRequired = parsePaymentRequired(paymentHeader);
412
+ const details = extractPaymentDetails(paymentRequired);
413
+ const extensions = paymentRequired.extensions;
414
+ const paymentPayload = await createPaymentPayload(
415
+ this.privateKey,
416
+ this.account.address,
417
+ details.recipient,
418
+ details.amount,
419
+ details.network || "eip155:8453",
420
+ {
421
+ resourceUrl: validateResourceUrl(
422
+ details.resource?.url || `${this.apiUrl}/v1/chat/completions`,
423
+ this.apiUrl
424
+ ),
425
+ resourceDescription: details.resource?.description || "BlockRun AI API call",
426
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
427
+ extra: details.extra,
428
+ extensions
429
+ }
430
+ );
431
+ const retryResponse = await this.fetchWithTimeout(url, {
432
+ method: "POST",
433
+ headers: {
434
+ "Content-Type": "application/json",
435
+ "PAYMENT-SIGNATURE": paymentPayload
436
+ },
437
+ body: JSON.stringify(body)
438
+ });
439
+ if (retryResponse.status === 402) {
440
+ throw new PaymentError("Payment was rejected. Check your wallet balance.");
441
+ }
442
+ if (!retryResponse.ok) {
443
+ let errorBody;
444
+ try {
445
+ errorBody = await retryResponse.json();
446
+ } catch {
447
+ errorBody = { error: "Request failed" };
448
+ }
449
+ throw new APIError(
450
+ `API error after payment: ${retryResponse.status}`,
451
+ retryResponse.status,
452
+ sanitizeErrorResponse(errorBody)
453
+ );
454
+ }
455
+ const costUsd = parseFloat(details.amount) / 1e6;
456
+ this.sessionCalls += 1;
457
+ this.sessionTotalUsd += costUsd;
458
+ return retryResponse.json();
459
+ }
460
+ /**
461
+ * Fetch with timeout.
462
+ */
463
+ async fetchWithTimeout(url, options) {
464
+ const controller = new AbortController();
465
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
466
+ try {
467
+ const response = await fetch(url, {
468
+ ...options,
469
+ signal: controller.signal
470
+ });
471
+ return response;
472
+ } finally {
473
+ clearTimeout(timeoutId);
474
+ }
475
+ }
476
+ /**
477
+ * List available LLM models with pricing.
478
+ */
479
+ async listModels() {
480
+ const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, {
481
+ method: "GET"
482
+ });
483
+ if (!response.ok) {
484
+ let errorBody;
485
+ try {
486
+ errorBody = await response.json();
487
+ } catch {
488
+ errorBody = { error: "Request failed" };
489
+ }
490
+ throw new APIError(
491
+ `Failed to list models: ${response.status}`,
492
+ response.status,
493
+ sanitizeErrorResponse(errorBody)
494
+ );
495
+ }
496
+ const data = await response.json();
497
+ return data.data || [];
498
+ }
499
+ /**
500
+ * List available image generation models with pricing.
501
+ */
502
+ async listImageModels() {
503
+ const response = await this.fetchWithTimeout(
504
+ `${this.apiUrl}/v1/images/models`,
505
+ { method: "GET" }
506
+ );
507
+ if (!response.ok) {
508
+ throw new APIError(
509
+ `Failed to list image models: ${response.status}`,
510
+ response.status
511
+ );
512
+ }
513
+ const data = await response.json();
514
+ return data.data || [];
515
+ }
516
+ /**
517
+ * List all available models (both LLM and image) with pricing.
518
+ *
519
+ * @returns Array of all models with 'type' field ('llm' or 'image')
520
+ *
521
+ * @example
522
+ * const models = await client.listAllModels();
523
+ * for (const model of models) {
524
+ * if (model.type === 'llm') {
525
+ * console.log(`LLM: ${model.id} - $${model.inputPrice}/M input`);
526
+ * } else {
527
+ * console.log(`Image: ${model.id} - $${model.pricePerImage}/image`);
528
+ * }
529
+ * }
530
+ */
531
+ async listAllModels() {
532
+ const llmModels = await this.listModels();
533
+ for (const model of llmModels) {
534
+ model.type = "llm";
535
+ }
536
+ const imageModels = await this.listImageModels();
537
+ for (const model of imageModels) {
538
+ model.type = "image";
539
+ }
540
+ return [...llmModels, ...imageModels];
541
+ }
542
+ /**
543
+ * Get current session spending.
544
+ *
545
+ * @returns Object with totalUsd and calls count
546
+ *
547
+ * @example
548
+ * const spending = client.getSpending();
549
+ * console.log(`Spent $${spending.totalUsd.toFixed(4)} across ${spending.calls} calls`);
550
+ */
551
+ getSpending() {
552
+ return {
553
+ totalUsd: this.sessionTotalUsd,
554
+ calls: this.sessionCalls
555
+ };
556
+ }
557
+ /**
558
+ * Get the wallet address being used for payments.
559
+ */
560
+ getWalletAddress() {
561
+ return this.account.address;
562
+ }
563
+ };
564
+ var client_default = LLMClient;
565
+
566
+ // src/image.ts
567
+ var import_accounts3 = require("viem/accounts");
568
+ var DEFAULT_API_URL2 = "https://blockrun.ai/api";
569
+ var DEFAULT_MODEL = "google/nano-banana";
570
+ var DEFAULT_SIZE = "1024x1024";
571
+ var DEFAULT_TIMEOUT2 = 12e4;
572
+ var ImageClient = class {
573
+ account;
574
+ privateKey;
575
+ apiUrl;
576
+ timeout;
577
+ sessionTotalUsd = 0;
578
+ sessionCalls = 0;
579
+ /**
580
+ * Initialize the BlockRun Image client.
581
+ *
582
+ * @param options - Client configuration options
583
+ */
584
+ constructor(options = {}) {
585
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
586
+ const privateKey = options.privateKey || envKey;
587
+ if (!privateKey) {
588
+ throw new Error(
589
+ "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
590
+ );
591
+ }
592
+ validatePrivateKey(privateKey);
593
+ this.privateKey = privateKey;
594
+ this.account = (0, import_accounts3.privateKeyToAccount)(privateKey);
595
+ const apiUrl = options.apiUrl || DEFAULT_API_URL2;
596
+ validateApiUrl(apiUrl);
597
+ this.apiUrl = apiUrl.replace(/\/$/, "");
598
+ this.timeout = options.timeout || DEFAULT_TIMEOUT2;
599
+ }
600
+ /**
601
+ * Generate an image from a text prompt.
602
+ *
603
+ * @param prompt - Text description of the image to generate
604
+ * @param options - Optional generation parameters
605
+ * @returns ImageResponse with generated image URLs
606
+ *
607
+ * @example
608
+ * const result = await client.generate('A sunset over mountains');
609
+ * console.log(result.data[0].url);
610
+ */
611
+ async generate(prompt, options) {
612
+ const body = {
613
+ model: options?.model || DEFAULT_MODEL,
614
+ prompt,
615
+ size: options?.size || DEFAULT_SIZE,
616
+ n: options?.n || 1
617
+ };
618
+ if (options?.quality) {
619
+ body.quality = options.quality;
620
+ }
621
+ return this.requestWithPayment("/v1/images/generations", body);
622
+ }
623
+ /**
624
+ * List available image generation models with pricing.
625
+ */
626
+ async listImageModels() {
627
+ const response = await this.fetchWithTimeout(
628
+ `${this.apiUrl}/v1/images/models`,
629
+ { method: "GET" }
630
+ );
631
+ if (!response.ok) {
632
+ throw new APIError(
633
+ `Failed to list image models: ${response.status}`,
634
+ response.status
635
+ );
636
+ }
637
+ const data = await response.json();
638
+ return data.data || [];
639
+ }
640
+ /**
641
+ * Make a request with automatic x402 payment handling.
642
+ */
643
+ async requestWithPayment(endpoint, body) {
644
+ const url = `${this.apiUrl}${endpoint}`;
645
+ const response = await this.fetchWithTimeout(url, {
646
+ method: "POST",
647
+ headers: { "Content-Type": "application/json" },
648
+ body: JSON.stringify(body)
649
+ });
650
+ if (response.status === 402) {
651
+ return this.handlePaymentAndRetry(url, body, response);
652
+ }
653
+ if (!response.ok) {
654
+ let errorBody;
655
+ try {
656
+ errorBody = await response.json();
657
+ } catch {
658
+ errorBody = { error: "Request failed" };
659
+ }
660
+ throw new APIError(
661
+ `API error: ${response.status}`,
662
+ response.status,
663
+ sanitizeErrorResponse(errorBody)
664
+ );
665
+ }
666
+ return response.json();
667
+ }
668
+ /**
669
+ * Handle 402 response: parse requirements, sign payment, retry.
670
+ */
671
+ async handlePaymentAndRetry(url, body, response) {
672
+ let paymentHeader = response.headers.get("payment-required");
673
+ if (!paymentHeader) {
674
+ try {
675
+ const respBody = await response.json();
676
+ if (respBody.x402 || respBody.accepts) {
677
+ paymentHeader = btoa(JSON.stringify(respBody));
678
+ }
679
+ } catch {
680
+ }
681
+ }
682
+ if (!paymentHeader) {
683
+ throw new PaymentError("402 response but no payment requirements found");
684
+ }
685
+ const paymentRequired = parsePaymentRequired(paymentHeader);
686
+ const details = extractPaymentDetails(paymentRequired);
687
+ const extensions = paymentRequired.extensions;
688
+ const paymentPayload = await createPaymentPayload(
689
+ this.privateKey,
690
+ this.account.address,
691
+ details.recipient,
692
+ details.amount,
693
+ details.network || "eip155:8453",
694
+ {
695
+ resourceUrl: validateResourceUrl(
696
+ details.resource?.url || `${this.apiUrl}/v1/images/generations`,
697
+ this.apiUrl
698
+ ),
699
+ resourceDescription: details.resource?.description || "BlockRun Image Generation",
700
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
701
+ extra: details.extra,
702
+ extensions
703
+ }
704
+ );
705
+ const retryResponse = await this.fetchWithTimeout(url, {
706
+ method: "POST",
707
+ headers: {
708
+ "Content-Type": "application/json",
709
+ "PAYMENT-SIGNATURE": paymentPayload
710
+ },
711
+ body: JSON.stringify(body)
712
+ });
713
+ if (retryResponse.status === 402) {
714
+ throw new PaymentError(
715
+ "Payment was rejected. Check your wallet balance."
716
+ );
717
+ }
718
+ if (!retryResponse.ok) {
719
+ let errorBody;
720
+ try {
721
+ errorBody = await retryResponse.json();
722
+ } catch {
723
+ errorBody = { error: "Request failed" };
724
+ }
725
+ throw new APIError(
726
+ `API error after payment: ${retryResponse.status}`,
727
+ retryResponse.status,
728
+ sanitizeErrorResponse(errorBody)
729
+ );
730
+ }
731
+ return retryResponse.json();
732
+ }
733
+ /**
734
+ * Fetch with timeout.
735
+ */
736
+ async fetchWithTimeout(url, options) {
737
+ const controller = new AbortController();
738
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
739
+ try {
740
+ const response = await fetch(url, {
741
+ ...options,
742
+ signal: controller.signal
743
+ });
744
+ return response;
745
+ } finally {
746
+ clearTimeout(timeoutId);
747
+ }
748
+ }
749
+ /**
750
+ * Get the wallet address being used for payments.
751
+ */
752
+ getWalletAddress() {
753
+ return this.account.address;
754
+ }
755
+ /**
756
+ * Get session spending information.
757
+ */
758
+ getSpending() {
759
+ return {
760
+ totalUsd: this.sessionTotalUsd,
761
+ calls: this.sessionCalls
762
+ };
763
+ }
764
+ };
765
+
766
+ // src/wallet.ts
767
+ var import_accounts4 = require("viem/accounts");
768
+ var fs = __toESM(require("fs"), 1);
769
+ var path = __toESM(require("path"), 1);
770
+ var os = __toESM(require("os"), 1);
771
+ var USDC_BASE_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
772
+ var BASE_CHAIN_ID2 = "8453";
773
+ var WALLET_DIR = path.join(os.homedir(), ".blockrun");
774
+ var WALLET_FILE = path.join(WALLET_DIR, ".session");
775
+ function createWallet() {
776
+ const privateKey = (0, import_accounts4.generatePrivateKey)();
777
+ const account = (0, import_accounts4.privateKeyToAccount)(privateKey);
778
+ return {
779
+ address: account.address,
780
+ privateKey
781
+ };
782
+ }
783
+ function saveWallet(privateKey) {
784
+ if (!fs.existsSync(WALLET_DIR)) {
785
+ fs.mkdirSync(WALLET_DIR, { recursive: true });
786
+ }
787
+ fs.writeFileSync(WALLET_FILE, privateKey, { mode: 384 });
788
+ return WALLET_FILE;
789
+ }
790
+ function loadWallet() {
791
+ if (fs.existsSync(WALLET_FILE)) {
792
+ const key = fs.readFileSync(WALLET_FILE, "utf-8").trim();
793
+ if (key) return key;
794
+ }
795
+ const legacyFile = path.join(WALLET_DIR, "wallet.key");
796
+ if (fs.existsSync(legacyFile)) {
797
+ const key = fs.readFileSync(legacyFile, "utf-8").trim();
798
+ if (key) return key;
799
+ }
800
+ return null;
801
+ }
802
+ function getOrCreateWallet() {
803
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
804
+ if (envKey) {
805
+ const account = (0, import_accounts4.privateKeyToAccount)(envKey);
806
+ return { address: account.address, privateKey: envKey, isNew: false };
807
+ }
808
+ const fileKey = loadWallet();
809
+ if (fileKey) {
810
+ const account = (0, import_accounts4.privateKeyToAccount)(fileKey);
811
+ return { address: account.address, privateKey: fileKey, isNew: false };
812
+ }
813
+ const { address, privateKey } = createWallet();
814
+ saveWallet(privateKey);
815
+ return { address, privateKey, isNew: true };
816
+ }
817
+ function getWalletAddress() {
818
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
819
+ if (envKey) {
820
+ return (0, import_accounts4.privateKeyToAccount)(envKey).address;
821
+ }
822
+ const fileKey = loadWallet();
823
+ if (fileKey) {
824
+ return (0, import_accounts4.privateKeyToAccount)(fileKey).address;
825
+ }
826
+ return null;
827
+ }
828
+ function getEip681Uri(address, amountUsdc = 1) {
829
+ const amountWei = Math.floor(amountUsdc * 1e6);
830
+ return `ethereum:${USDC_BASE_CONTRACT}@${BASE_CHAIN_ID2}/transfer?address=${address}&uint256=${amountWei}`;
831
+ }
832
+ function getPaymentLinks(address) {
833
+ return {
834
+ basescan: `https://basescan.org/address/${address}`,
835
+ walletLink: `ethereum:${USDC_BASE_CONTRACT}@${BASE_CHAIN_ID2}/transfer?address=${address}`,
836
+ ethereum: `ethereum:${address}@${BASE_CHAIN_ID2}`,
837
+ blockrun: `https://blockrun.ai/fund?address=${address}`
838
+ };
839
+ }
840
+ function formatWalletCreatedMessage(address) {
841
+ const links = getPaymentLinks(address);
842
+ return `
843
+ I'm your BlockRun Agent! I can access GPT-4, Grok, image generation, and more.
844
+
845
+ Please send $1-5 USDC on Base to start:
846
+
847
+ ${address}
848
+
849
+ What is Base? Base is Coinbase's blockchain network.
850
+ You can buy USDC on Coinbase and send it directly to me.
851
+
852
+ What $1 USDC gets you:
853
+ - ~1,000 GPT-4o calls
854
+ - ~100 image generations
855
+ - ~10,000 DeepSeek calls
856
+
857
+ Quick links:
858
+ - Check my balance: ${links.basescan}
859
+ - Get USDC: https://www.coinbase.com or https://bridge.base.org
860
+
861
+ Questions? care@blockrun.ai | Issues? github.com/BlockRunAI/blockrun-llm-ts/issues
862
+
863
+ Key stored securely in ~/.blockrun/
864
+ Your private key never leaves your machine - only signatures are sent.
865
+ `;
866
+ }
867
+ function formatNeedsFundingMessage(address) {
868
+ const links = getPaymentLinks(address);
869
+ return `
870
+ I've run out of funds! Please send more USDC on Base to continue helping you.
871
+
872
+ Send to my address:
873
+ ${address}
874
+
875
+ Check my balance: ${links.basescan}
876
+
877
+ What $1 USDC gets you: ~1,000 GPT-4o calls or ~100 images.
878
+ Questions? care@blockrun.ai | Issues? github.com/BlockRunAI/blockrun-llm-ts/issues
879
+
880
+ Your private key never leaves your machine - only signatures are sent.
881
+ `;
882
+ }
883
+ function formatFundingMessageCompact(address) {
884
+ const links = getPaymentLinks(address);
885
+ return `I need a little top-up to keep helping you! Send USDC on Base to: ${address}
886
+ Check my balance: ${links.basescan}`;
887
+ }
888
+ var WALLET_FILE_PATH = WALLET_FILE;
889
+ var WALLET_DIR_PATH = WALLET_DIR;
890
+
891
+ // src/openai-compat.ts
892
+ var StreamingResponse = class {
893
+ reader;
894
+ decoder;
895
+ buffer = "";
896
+ model;
897
+ id;
898
+ constructor(response, model) {
899
+ if (!response.body) {
900
+ throw new Error("Response body is null");
901
+ }
902
+ this.reader = response.body.getReader();
903
+ this.decoder = new TextDecoder();
904
+ this.model = model;
905
+ this.id = `chatcmpl-${Date.now()}`;
906
+ }
907
+ async *[Symbol.asyncIterator]() {
908
+ try {
909
+ while (true) {
910
+ const { done, value } = await this.reader.read();
911
+ if (done) break;
912
+ this.buffer += this.decoder.decode(value, { stream: true });
913
+ const lines = this.buffer.split("\n");
914
+ this.buffer = lines.pop() || "";
915
+ for (const line of lines) {
916
+ const trimmed = line.trim();
917
+ if (!trimmed || !trimmed.startsWith("data: ")) continue;
918
+ const data = trimmed.slice(6);
919
+ if (data === "[DONE]") return;
920
+ try {
921
+ const parsed = JSON.parse(data);
922
+ yield this.transformChunk(parsed);
923
+ } catch {
924
+ }
925
+ }
926
+ }
927
+ } finally {
928
+ this.reader.releaseLock();
929
+ }
930
+ }
931
+ transformChunk(data) {
932
+ const choices = data.choices || [];
933
+ return {
934
+ id: data.id || this.id,
935
+ object: "chat.completion.chunk",
936
+ created: data.created || Math.floor(Date.now() / 1e3),
937
+ model: data.model || this.model,
938
+ choices: choices.map((choice, index) => ({
939
+ index: choice.index ?? index,
940
+ delta: {
941
+ role: choice.delta?.role,
942
+ content: choice.delta?.content
943
+ },
944
+ finish_reason: choice.finish_reason || null
945
+ }))
946
+ };
947
+ }
948
+ };
949
+ var ChatCompletions = class {
950
+ constructor(client, apiUrl, timeout) {
951
+ this.client = client;
952
+ this.apiUrl = apiUrl;
953
+ this.timeout = timeout;
954
+ }
955
+ async create(params) {
956
+ if (params.stream) {
957
+ return this.createStream(params);
958
+ }
959
+ const response = await this.client.chatCompletion(
960
+ params.model,
961
+ params.messages,
962
+ {
963
+ maxTokens: params.max_tokens,
964
+ temperature: params.temperature,
965
+ topP: params.top_p
966
+ }
967
+ );
968
+ return this.transformResponse(response);
969
+ }
970
+ async createStream(params) {
971
+ const url = `${this.apiUrl}/v1/chat/completions`;
972
+ const body = {
973
+ model: params.model,
974
+ messages: params.messages,
975
+ max_tokens: params.max_tokens || 1024,
976
+ temperature: params.temperature,
977
+ top_p: params.top_p,
978
+ stream: true
979
+ };
980
+ const controller = new AbortController();
981
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
982
+ try {
983
+ const response = await fetch(url, {
984
+ method: "POST",
985
+ headers: { "Content-Type": "application/json" },
986
+ body: JSON.stringify(body),
987
+ signal: controller.signal
988
+ });
989
+ if (response.status === 402) {
990
+ const paymentHeader = response.headers.get("payment-required");
991
+ if (!paymentHeader) {
992
+ throw new Error("402 response but no payment requirements found");
993
+ }
994
+ throw new Error(
995
+ "Streaming with automatic payment requires direct wallet access. Please use non-streaming mode or contact support for streaming setup."
996
+ );
997
+ }
998
+ if (!response.ok) {
999
+ throw new Error(`API error: ${response.status}`);
1000
+ }
1001
+ return new StreamingResponse(response, params.model);
1002
+ } finally {
1003
+ clearTimeout(timeoutId);
1004
+ }
1005
+ }
1006
+ transformResponse(response) {
1007
+ return {
1008
+ id: response.id || `chatcmpl-${Date.now()}`,
1009
+ object: "chat.completion",
1010
+ created: response.created || Math.floor(Date.now() / 1e3),
1011
+ model: response.model,
1012
+ choices: response.choices.map((choice, index) => ({
1013
+ index: choice.index ?? index,
1014
+ message: {
1015
+ role: "assistant",
1016
+ content: choice.message.content
1017
+ },
1018
+ finish_reason: choice.finish_reason || "stop"
1019
+ })),
1020
+ usage: response.usage
1021
+ };
1022
+ }
1023
+ };
1024
+ var Chat = class {
1025
+ completions;
1026
+ constructor(client, apiUrl, timeout) {
1027
+ this.completions = new ChatCompletions(client, apiUrl, timeout);
1028
+ }
1029
+ };
1030
+ var OpenAI = class {
1031
+ chat;
1032
+ client;
1033
+ constructor(options = {}) {
1034
+ const privateKey = options.walletKey || options.privateKey;
1035
+ const apiUrl = options.baseURL || "https://blockrun.ai/api";
1036
+ const timeout = options.timeout || 6e4;
1037
+ this.client = new LLMClient({
1038
+ privateKey,
1039
+ apiUrl,
1040
+ timeout
1041
+ });
1042
+ this.chat = new Chat(this.client, apiUrl, timeout);
1043
+ }
1044
+ /**
1045
+ * Get the wallet address being used for payments.
1046
+ */
1047
+ getWalletAddress() {
1048
+ return this.client.getWalletAddress();
1049
+ }
1050
+ };
1051
+ // Annotate the CommonJS export names for ESM import in node:
1052
+ 0 && (module.exports = {
1053
+ APIError,
1054
+ BASE_CHAIN_ID,
1055
+ BlockrunError,
1056
+ ImageClient,
1057
+ LLMClient,
1058
+ OpenAI,
1059
+ PaymentError,
1060
+ USDC_BASE,
1061
+ USDC_BASE_CONTRACT,
1062
+ WALLET_DIR_PATH,
1063
+ WALLET_FILE_PATH,
1064
+ createWallet,
1065
+ formatFundingMessageCompact,
1066
+ formatNeedsFundingMessage,
1067
+ formatWalletCreatedMessage,
1068
+ getEip681Uri,
1069
+ getOrCreateWallet,
1070
+ getPaymentLinks,
1071
+ getWalletAddress,
1072
+ loadWallet,
1073
+ saveWallet
1074
+ });