@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/{chunk-R7MQBF5Y.mjs → chunk-4LEERQOK.js} +1 -1
- package/dist/{esm-3BUZROFC.mjs → esm-W3HWUFAT.js} +2 -2
- package/dist/index.cjs +1074 -0
- package/dist/index.d.cts +587 -0
- package/dist/index.d.ts +294 -2
- package/dist/{index.esm-FLTVMT4C.mjs → index.esm-DGL5QIKD.js} +2 -2
- package/dist/index.js +490 -36439
- package/package.json +11 -6
- package/dist/chunk-FCKQTBEP.mjs +0 -90
- package/dist/chunk-TYQEUMVI.mjs +0 -206
- package/dist/index.d.mts +0 -295
- package/dist/index.mjs +0 -428
- package/dist/validation-FPOEEOR4.mjs +0 -15
- package/dist/x402-ELHVFRUU.mjs +0 -27
- /package/dist/{chunk-HEBXNMVQ.mjs → chunk-2ESYSVXG.js} +0 -0
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
|
+
});
|