@blockrun/llm 0.1.1

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.mjs ADDED
@@ -0,0 +1,428 @@
1
+ import {
2
+ BASE_CHAIN_ID,
3
+ USDC_BASE,
4
+ createPaymentPayload,
5
+ extractPaymentDetails,
6
+ parsePaymentRequired
7
+ } from "./chunk-TYQEUMVI.mjs";
8
+ import {
9
+ sanitizeErrorResponse,
10
+ validateApiUrl,
11
+ validatePrivateKey,
12
+ validateResourceUrl
13
+ } from "./chunk-FCKQTBEP.mjs";
14
+ import "./chunk-HEBXNMVQ.mjs";
15
+
16
+ // src/client.ts
17
+ import { privateKeyToAccount } from "viem/accounts";
18
+
19
+ // src/types.ts
20
+ var BlockrunError = class extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = "BlockrunError";
24
+ }
25
+ };
26
+ var PaymentError = class extends BlockrunError {
27
+ constructor(message) {
28
+ super(message);
29
+ this.name = "PaymentError";
30
+ }
31
+ };
32
+ var APIError = class extends BlockrunError {
33
+ statusCode;
34
+ response;
35
+ constructor(message, statusCode, response) {
36
+ super(message);
37
+ this.name = "APIError";
38
+ this.statusCode = statusCode;
39
+ this.response = response;
40
+ }
41
+ };
42
+
43
+ // src/client.ts
44
+ var DEFAULT_API_URL = "https://blockrun.ai/api";
45
+ var DEFAULT_MAX_TOKENS = 1024;
46
+ var DEFAULT_TIMEOUT = 6e4;
47
+ var LLMClient = class {
48
+ account;
49
+ privateKey;
50
+ apiUrl;
51
+ timeout;
52
+ /**
53
+ * Initialize the BlockRun LLM client.
54
+ *
55
+ * @param options - Client configuration options (optional if BASE_CHAIN_WALLET_KEY env var is set)
56
+ */
57
+ constructor(options = {}) {
58
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BASE_CHAIN_WALLET_KEY : void 0;
59
+ const privateKey = options.privateKey || envKey;
60
+ if (!privateKey) {
61
+ throw new Error(
62
+ "Private key required. Pass privateKey in options or set BASE_CHAIN_WALLET_KEY environment variable."
63
+ );
64
+ }
65
+ validatePrivateKey(privateKey);
66
+ this.privateKey = privateKey;
67
+ this.account = privateKeyToAccount(privateKey);
68
+ const apiUrl = options.apiUrl || DEFAULT_API_URL;
69
+ validateApiUrl(apiUrl);
70
+ this.apiUrl = apiUrl.replace(/\/$/, "");
71
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
72
+ }
73
+ /**
74
+ * Simple 1-line chat interface.
75
+ *
76
+ * @param model - Model ID (e.g., 'openai/gpt-4o', 'anthropic/claude-sonnet-4')
77
+ * @param prompt - User message
78
+ * @param options - Optional chat parameters
79
+ * @returns Assistant's response text
80
+ *
81
+ * @example
82
+ * const response = await client.chat('gpt-4o', 'What is the capital of France?');
83
+ * console.log(response); // 'The capital of France is Paris.'
84
+ */
85
+ async chat(model, prompt, options) {
86
+ const messages = [];
87
+ if (options?.system) {
88
+ messages.push({ role: "system", content: options.system });
89
+ }
90
+ messages.push({ role: "user", content: prompt });
91
+ const result = await this.chatCompletion(model, messages, {
92
+ maxTokens: options?.maxTokens,
93
+ temperature: options?.temperature,
94
+ topP: options?.topP
95
+ });
96
+ return result.choices[0].message.content;
97
+ }
98
+ /**
99
+ * Full chat completion interface (OpenAI-compatible).
100
+ *
101
+ * @param model - Model ID
102
+ * @param messages - Array of messages with role and content
103
+ * @param options - Optional completion parameters
104
+ * @returns ChatResponse object with choices and usage
105
+ */
106
+ async chatCompletion(model, messages, options) {
107
+ const body = {
108
+ model,
109
+ messages,
110
+ max_tokens: options?.maxTokens || DEFAULT_MAX_TOKENS
111
+ };
112
+ if (options?.temperature !== void 0) {
113
+ body.temperature = options.temperature;
114
+ }
115
+ if (options?.topP !== void 0) {
116
+ body.top_p = options.topP;
117
+ }
118
+ return this.requestWithPayment("/v1/chat/completions", body);
119
+ }
120
+ /**
121
+ * Make a request with automatic x402 payment handling.
122
+ */
123
+ async requestWithPayment(endpoint, body) {
124
+ const url = `${this.apiUrl}${endpoint}`;
125
+ const response = await this.fetchWithTimeout(url, {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify(body)
129
+ });
130
+ if (response.status === 402) {
131
+ return this.handlePaymentAndRetry(url, body, response);
132
+ }
133
+ if (!response.ok) {
134
+ let errorBody;
135
+ try {
136
+ errorBody = await response.json();
137
+ } catch {
138
+ errorBody = { error: "Request failed" };
139
+ }
140
+ throw new APIError(
141
+ `API error: ${response.status}`,
142
+ response.status,
143
+ sanitizeErrorResponse(errorBody)
144
+ );
145
+ }
146
+ return response.json();
147
+ }
148
+ /**
149
+ * Handle 402 response: parse requirements, sign payment, retry.
150
+ */
151
+ async handlePaymentAndRetry(url, body, response) {
152
+ let paymentHeader = response.headers.get("payment-required");
153
+ if (!paymentHeader) {
154
+ try {
155
+ const respBody = await response.json();
156
+ if (respBody.x402 || respBody.accepts) {
157
+ paymentHeader = btoa(JSON.stringify(respBody));
158
+ }
159
+ } catch (parseError) {
160
+ console.debug("Failed to parse payment header from response body", parseError);
161
+ }
162
+ }
163
+ if (!paymentHeader) {
164
+ throw new PaymentError("402 response but no payment requirements found");
165
+ }
166
+ const paymentRequired = parsePaymentRequired(paymentHeader);
167
+ const details = extractPaymentDetails(paymentRequired);
168
+ const extensions = paymentRequired.extensions;
169
+ const paymentPayload = await createPaymentPayload(
170
+ this.privateKey,
171
+ this.account.address,
172
+ details.recipient,
173
+ details.amount,
174
+ details.network || "eip155:8453",
175
+ {
176
+ resourceUrl: validateResourceUrl(
177
+ details.resource?.url || `${this.apiUrl}/v1/chat/completions`,
178
+ this.apiUrl
179
+ ),
180
+ resourceDescription: details.resource?.description || "BlockRun AI API call",
181
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
182
+ extra: details.extra,
183
+ extensions
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
+ if (retryResponse.status === 402) {
195
+ throw new PaymentError("Payment was rejected. Check your wallet balance.");
196
+ }
197
+ if (!retryResponse.ok) {
198
+ let errorBody;
199
+ try {
200
+ errorBody = await retryResponse.json();
201
+ } catch {
202
+ errorBody = { error: "Request failed" };
203
+ }
204
+ throw new APIError(
205
+ `API error after payment: ${retryResponse.status}`,
206
+ retryResponse.status,
207
+ sanitizeErrorResponse(errorBody)
208
+ );
209
+ }
210
+ return retryResponse.json();
211
+ }
212
+ /**
213
+ * Fetch with timeout.
214
+ */
215
+ async fetchWithTimeout(url, options) {
216
+ const controller = new AbortController();
217
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
218
+ try {
219
+ const response = await fetch(url, {
220
+ ...options,
221
+ signal: controller.signal
222
+ });
223
+ return response;
224
+ } finally {
225
+ clearTimeout(timeoutId);
226
+ }
227
+ }
228
+ /**
229
+ * List available models with pricing.
230
+ */
231
+ async listModels() {
232
+ const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, {
233
+ method: "GET"
234
+ });
235
+ if (!response.ok) {
236
+ throw new APIError(
237
+ `Failed to list models: ${response.status}`,
238
+ response.status
239
+ );
240
+ }
241
+ const data = await response.json();
242
+ return data.data || [];
243
+ }
244
+ /**
245
+ * Get the wallet address being used for payments.
246
+ */
247
+ getWalletAddress() {
248
+ return this.account.address;
249
+ }
250
+ };
251
+ var client_default = LLMClient;
252
+
253
+ // src/openai-compat.ts
254
+ var StreamingResponse = class {
255
+ reader;
256
+ decoder;
257
+ buffer = "";
258
+ model;
259
+ id;
260
+ constructor(response, model) {
261
+ if (!response.body) {
262
+ throw new Error("Response body is null");
263
+ }
264
+ this.reader = response.body.getReader();
265
+ this.decoder = new TextDecoder();
266
+ this.model = model;
267
+ this.id = `chatcmpl-${Date.now()}`;
268
+ }
269
+ async *[Symbol.asyncIterator]() {
270
+ try {
271
+ while (true) {
272
+ const { done, value } = await this.reader.read();
273
+ if (done) break;
274
+ this.buffer += this.decoder.decode(value, { stream: true });
275
+ const lines = this.buffer.split("\n");
276
+ this.buffer = lines.pop() || "";
277
+ for (const line of lines) {
278
+ const trimmed = line.trim();
279
+ if (!trimmed || !trimmed.startsWith("data: ")) continue;
280
+ const data = trimmed.slice(6);
281
+ if (data === "[DONE]") return;
282
+ try {
283
+ const parsed = JSON.parse(data);
284
+ yield this.transformChunk(parsed);
285
+ } catch {
286
+ }
287
+ }
288
+ }
289
+ } finally {
290
+ this.reader.releaseLock();
291
+ }
292
+ }
293
+ transformChunk(data) {
294
+ const choices = data.choices || [];
295
+ return {
296
+ id: data.id || this.id,
297
+ object: "chat.completion.chunk",
298
+ created: data.created || Math.floor(Date.now() / 1e3),
299
+ model: data.model || this.model,
300
+ choices: choices.map((choice, index) => ({
301
+ index: choice.index ?? index,
302
+ delta: {
303
+ role: choice.delta?.role,
304
+ content: choice.delta?.content
305
+ },
306
+ finish_reason: choice.finish_reason || null
307
+ }))
308
+ };
309
+ }
310
+ };
311
+ var ChatCompletions = class {
312
+ constructor(client, apiUrl, timeout) {
313
+ this.client = client;
314
+ this.apiUrl = apiUrl;
315
+ this.timeout = timeout;
316
+ }
317
+ async create(params) {
318
+ if (params.stream) {
319
+ return this.createStream(params);
320
+ }
321
+ const response = await this.client.chatCompletion(
322
+ params.model,
323
+ params.messages,
324
+ {
325
+ maxTokens: params.max_tokens,
326
+ temperature: params.temperature,
327
+ topP: params.top_p
328
+ }
329
+ );
330
+ return this.transformResponse(response);
331
+ }
332
+ async createStream(params) {
333
+ const url = `${this.apiUrl}/v1/chat/completions`;
334
+ const body = {
335
+ model: params.model,
336
+ messages: params.messages,
337
+ max_tokens: params.max_tokens || 1024,
338
+ temperature: params.temperature,
339
+ top_p: params.top_p,
340
+ stream: true
341
+ };
342
+ const controller = new AbortController();
343
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
344
+ try {
345
+ let response = await fetch(url, {
346
+ method: "POST",
347
+ headers: { "Content-Type": "application/json" },
348
+ body: JSON.stringify(body),
349
+ signal: controller.signal
350
+ });
351
+ if (response.status === 402) {
352
+ const paymentHeader = response.headers.get("payment-required");
353
+ if (!paymentHeader) {
354
+ throw new Error("402 response but no payment requirements found");
355
+ }
356
+ const { createPaymentPayload: createPaymentPayload2, parsePaymentRequired: parsePaymentRequired2, extractPaymentDetails: extractPaymentDetails2 } = await import("./x402-ELHVFRUU.mjs");
357
+ const { validateResourceUrl: validateResourceUrl2 } = await import("./validation-FPOEEOR4.mjs");
358
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
359
+ const paymentRequired = parsePaymentRequired2(paymentHeader);
360
+ const details = extractPaymentDetails2(paymentRequired);
361
+ const walletAddress = this.client.getWalletAddress();
362
+ throw new Error(
363
+ "Streaming with automatic payment requires direct wallet access. Please use non-streaming mode or contact support for streaming setup."
364
+ );
365
+ }
366
+ if (!response.ok) {
367
+ throw new Error(`API error: ${response.status}`);
368
+ }
369
+ return new StreamingResponse(response, params.model);
370
+ } finally {
371
+ clearTimeout(timeoutId);
372
+ }
373
+ }
374
+ transformResponse(response) {
375
+ return {
376
+ id: response.id || `chatcmpl-${Date.now()}`,
377
+ object: "chat.completion",
378
+ created: response.created || Math.floor(Date.now() / 1e3),
379
+ model: response.model,
380
+ choices: response.choices.map((choice, index) => ({
381
+ index: choice.index ?? index,
382
+ message: {
383
+ role: "assistant",
384
+ content: choice.message.content
385
+ },
386
+ finish_reason: choice.finish_reason || "stop"
387
+ })),
388
+ usage: response.usage
389
+ };
390
+ }
391
+ };
392
+ var Chat = class {
393
+ completions;
394
+ constructor(client, apiUrl, timeout) {
395
+ this.completions = new ChatCompletions(client, apiUrl, timeout);
396
+ }
397
+ };
398
+ var OpenAI = class {
399
+ chat;
400
+ client;
401
+ constructor(options = {}) {
402
+ const privateKey = options.walletKey || options.privateKey;
403
+ const apiUrl = options.baseURL || "https://blockrun.ai/api";
404
+ const timeout = options.timeout || 6e4;
405
+ this.client = new LLMClient({
406
+ privateKey,
407
+ apiUrl,
408
+ timeout
409
+ });
410
+ this.chat = new Chat(this.client, apiUrl, timeout);
411
+ }
412
+ /**
413
+ * Get the wallet address being used for payments.
414
+ */
415
+ getWalletAddress() {
416
+ return this.client.getWalletAddress();
417
+ }
418
+ };
419
+ export {
420
+ APIError,
421
+ BASE_CHAIN_ID,
422
+ BlockrunError,
423
+ LLMClient,
424
+ OpenAI,
425
+ PaymentError,
426
+ USDC_BASE,
427
+ client_default as default
428
+ };
@@ -0,0 +1,15 @@
1
+ import {
2
+ extractPrivateKey,
3
+ sanitizeErrorResponse,
4
+ validateApiUrl,
5
+ validatePrivateKey,
6
+ validateResourceUrl
7
+ } from "./chunk-FCKQTBEP.mjs";
8
+ import "./chunk-HEBXNMVQ.mjs";
9
+ export {
10
+ extractPrivateKey,
11
+ sanitizeErrorResponse,
12
+ validateApiUrl,
13
+ validatePrivateKey,
14
+ validateResourceUrl
15
+ };
@@ -0,0 +1,27 @@
1
+ import {
2
+ BASE_CHAIN_ID,
3
+ SOLANA_NETWORK,
4
+ SOLANA_USDC_DECIMALS,
5
+ USDC_BASE,
6
+ USDC_SOLANA,
7
+ createPaymentPayload,
8
+ createSolanaPaymentPayload,
9
+ extractPaymentDetails,
10
+ getAvailableNetworks,
11
+ isSolanaNetwork,
12
+ parsePaymentRequired
13
+ } from "./chunk-TYQEUMVI.mjs";
14
+ import "./chunk-HEBXNMVQ.mjs";
15
+ export {
16
+ BASE_CHAIN_ID,
17
+ SOLANA_NETWORK,
18
+ SOLANA_USDC_DECIMALS,
19
+ USDC_BASE,
20
+ USDC_SOLANA,
21
+ createPaymentPayload,
22
+ createSolanaPaymentPayload,
23
+ extractPaymentDetails,
24
+ getAvailableNetworks,
25
+ isSolanaNetwork,
26
+ parsePaymentRequired
27
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@blockrun/llm",
3
+ "version": "0.1.1",
4
+ "description": "BlockRun LLM Gateway SDK - Pay-per-request AI via x402 on Base and Solana",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "test": "vitest",
23
+ "lint": "eslint src/",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "keywords": [
27
+ "llm",
28
+ "ai",
29
+ "x402",
30
+ "base",
31
+ "solana",
32
+ "usdc",
33
+ "micropayments",
34
+ "openai",
35
+ "claude",
36
+ "gemini",
37
+ "blockchain"
38
+ ],
39
+ "author": "BlockRun <hello@blockrun.ai>",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/BlockRunAI/blockrun-llm-ts"
44
+ },
45
+ "homepage": "https://blockrun.ai",
46
+ "bugs": {
47
+ "url": "https://github.com/BlockRunAI/blockrun-llm-ts/issues"
48
+ },
49
+ "dependencies": {
50
+ "viem": "^2.21.0"
51
+ },
52
+ "optionalDependencies": {
53
+ "@solana/web3.js": "^1.98.0",
54
+ "@solana/spl-token": "^0.4.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^20.0.0",
58
+ "tsup": "^8.0.0",
59
+ "typescript": "^5.0.0",
60
+ "vitest": "^1.0.0"
61
+ },
62
+ "engines": {
63
+ "node": ">=18"
64
+ }
65
+ }