@blockrun/llm 1.15.0 → 2.0.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.js CHANGED
@@ -26,7 +26,7 @@ var APIError = class extends BlockrunError {
26
26
  };
27
27
 
28
28
  // src/client.ts
29
- import { route, DEFAULT_ROUTING_CONFIG } from "@blockrun/clawrouter";
29
+ import { route, DEFAULT_ROUTING_CONFIG, getFallbackChain } from "@blockrun/clawrouter";
30
30
 
31
31
  // src/x402.ts
32
32
  import { signTypedData } from "viem/accounts";
@@ -341,16 +341,107 @@ function validateResourceUrl(url, baseUrl) {
341
341
  }
342
342
  }
343
343
 
344
+ // src/cost-log.ts
345
+ import * as fs from "fs";
346
+ import * as path from "path";
347
+ import * as os from "os";
348
+ var BLOCKRUN_DIR = path.join(os.homedir(), ".blockrun");
349
+ var COST_LOG_FILE = path.join(BLOCKRUN_DIR, "cost_log.jsonl");
350
+ function logCost(entry) {
351
+ try {
352
+ fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
353
+ } catch {
354
+ }
355
+ try {
356
+ fs.appendFileSync(COST_LOG_FILE, JSON.stringify(entry) + "\n");
357
+ } catch {
358
+ }
359
+ }
360
+ function getCostSummary() {
361
+ if (!fs.existsSync(COST_LOG_FILE)) {
362
+ return { totalUsd: 0, calls: 0, byModel: {}, byEndpoint: {} };
363
+ }
364
+ let totalUsd = 0;
365
+ let calls = 0;
366
+ const byModel = {};
367
+ const byEndpoint = {};
368
+ try {
369
+ const content = fs.readFileSync(COST_LOG_FILE, "utf-8").trim();
370
+ if (!content) return { totalUsd: 0, calls: 0, byModel: {}, byEndpoint: {} };
371
+ for (const line of content.split("\n")) {
372
+ if (!line) continue;
373
+ try {
374
+ const raw = JSON.parse(line);
375
+ const cost = typeof raw.cost_usd === "number" ? raw.cost_usd : raw.costUsd ?? 0;
376
+ if (!cost) continue;
377
+ totalUsd += cost;
378
+ calls += 1;
379
+ if (raw.model) byModel[raw.model] = (byModel[raw.model] || 0) + cost;
380
+ if (raw.endpoint) byEndpoint[raw.endpoint] = (byEndpoint[raw.endpoint] || 0) + cost;
381
+ } catch {
382
+ }
383
+ }
384
+ } catch {
385
+ }
386
+ return { totalUsd, calls, byModel, byEndpoint };
387
+ }
388
+
344
389
  // src/client.ts
390
+ function isTransientError(err) {
391
+ if (err instanceof PaymentError) return false;
392
+ if (err instanceof APIError) {
393
+ return [502, 503, 504, 522, 524].includes(err.statusCode);
394
+ }
395
+ if (err instanceof Error) {
396
+ if (err.name === "AbortError") return true;
397
+ if (err.name === "TypeError" && /fetch|network/i.test(err.message)) return true;
398
+ }
399
+ return false;
400
+ }
401
+ function errSummary(err) {
402
+ if (err instanceof APIError) return `APIError ${err.statusCode}`;
403
+ if (err instanceof Error) {
404
+ const msg = err.message.length > 80 ? err.message.slice(0, 80) : err.message;
405
+ return `${err.name}: ${msg}`;
406
+ }
407
+ return String(err).slice(0, 100);
408
+ }
409
+ function mapRawToModel(m) {
410
+ return {
411
+ id: m.id,
412
+ name: m.name || m.id,
413
+ provider: m.provider || m.owned_by || "",
414
+ description: m.description || "",
415
+ inputPrice: m.inputPrice ?? m.input_price ?? m.pricing?.input ?? 0,
416
+ outputPrice: m.outputPrice ?? m.output_price ?? m.pricing?.output ?? 0,
417
+ contextWindow: m.contextWindow ?? m.context_window ?? 0,
418
+ maxOutput: m.maxOutput ?? m.max_output ?? 0,
419
+ categories: m.categories || [],
420
+ available: true,
421
+ billingMode: m.billingMode ?? m.billing_mode,
422
+ flatPrice: m.flatPrice ?? m.flat_price ?? m.pricing?.flat,
423
+ hidden: m.hidden
424
+ };
425
+ }
426
+ function mapRawToImageModel(m) {
427
+ return {
428
+ id: m.id,
429
+ name: m.name || m.id,
430
+ provider: m.provider || m.owned_by || "",
431
+ description: m.description || "",
432
+ pricePerImage: m.pricePerImage ?? m.price_per_image ?? m.pricing?.flat ?? m.flatPrice ?? m.flat_price ?? 0,
433
+ supportedSizes: m.supportedSizes ?? m.supported_sizes,
434
+ maxPromptLength: m.maxPromptLength ?? m.max_prompt_length,
435
+ available: true
436
+ };
437
+ }
345
438
  var DEFAULT_API_URL = "https://blockrun.ai/api";
346
- var TESTNET_API_URL = "https://testnet.blockrun.ai/api";
347
439
  var DEFAULT_MAX_TOKENS = 1024;
348
440
  var DEFAULT_TIMEOUT = 6e4;
349
441
  var SDK_VERSION = "1.5.0";
350
442
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
351
443
  var LLMClient = class _LLMClient {
352
444
  static DEFAULT_API_URL = DEFAULT_API_URL;
353
- static TESTNET_API_URL = TESTNET_API_URL;
354
445
  account;
355
446
  privateKey;
356
447
  apiUrl;
@@ -408,7 +499,8 @@ var LLMClient = class _LLMClient {
408
499
  temperature: options?.temperature,
409
500
  topP: options?.topP,
410
501
  search: options?.search,
411
- searchParameters: options?.searchParameters
502
+ searchParameters: options?.searchParameters,
503
+ fallbackModels: options?.fallbackModels
412
504
  });
413
505
  return result.choices[0].message.content || "";
414
506
  }
@@ -450,18 +542,24 @@ var LLMClient = class _LLMClient {
450
542
  modelPricing,
451
543
  routingProfile: options?.routingProfile
452
544
  });
545
+ const tierConfigs = decision.tierConfigs ?? DEFAULT_ROUTING_CONFIG.tiers;
546
+ const fullChain = getFallbackChain(decision.tier, tierConfigs);
547
+ const fallbacks = fullChain.filter(
548
+ (id) => id !== decision.model && modelPricing.has(id)
549
+ );
453
550
  const response = await this.chat(decision.model, prompt, {
454
551
  system: options?.system,
455
552
  maxTokens: options?.maxTokens,
456
553
  temperature: options?.temperature,
457
554
  topP: options?.topP,
458
555
  search: options?.search,
459
- searchParameters: options?.searchParameters
556
+ searchParameters: options?.searchParameters,
557
+ fallbackModels: fallbacks
460
558
  });
461
559
  return {
462
560
  response,
463
561
  model: decision.model,
464
- routing: decision
562
+ routing: { ...decision, fallbacks }
465
563
  };
466
564
  }
467
565
  /**
@@ -485,50 +583,108 @@ var LLMClient = class _LLMClient {
485
583
  }
486
584
  /**
487
585
  * Fetch model pricing from API.
586
+ *
587
+ * For flat-billed models (e.g. ZAI GLM-5 family at $0.001/call) the
588
+ * router still expects per-token rates, so we synthesise an equivalent
589
+ * per-token price assuming ~1500 total tokens per call. Without this,
590
+ * flat models would resolve to inputPrice=outputPrice=0 and the router
591
+ * would treat them as free, biasing routing decisions and reporting
592
+ * inflated savings %.
488
593
  */
489
594
  async fetchModelPricing() {
490
595
  const models = await this.listModels();
491
596
  const pricing = /* @__PURE__ */ new Map();
492
597
  for (const model of models) {
493
- pricing.set(model.id, {
494
- inputPrice: model.inputPrice,
495
- outputPrice: model.outputPrice
496
- });
598
+ if (model.billingMode === "flat" && model.flatPrice && model.flatPrice > 0) {
599
+ const perDirection = model.flatPrice * 1e6 / 1500 / 2;
600
+ pricing.set(model.id, {
601
+ inputPrice: perDirection,
602
+ outputPrice: perDirection
603
+ });
604
+ } else {
605
+ pricing.set(model.id, {
606
+ inputPrice: model.inputPrice,
607
+ outputPrice: model.outputPrice
608
+ });
609
+ }
497
610
  }
498
611
  return pricing;
499
612
  }
500
613
  /**
501
614
  * Full chat completion interface (OpenAI-compatible).
502
615
  *
503
- * @param model - Model ID
616
+ * When `fallbackModels` is set, transient failures (timeouts, network
617
+ * errors, 5xx) on the primary model trigger a retry against the next
618
+ * model in the list before raising. 4xx errors and PaymentError
619
+ * propagate immediately — those aren't "swap upstream and retry"
620
+ * situations. Each fallback hop logs one stderr line.
621
+ *
622
+ * @param model - Primary model ID
504
623
  * @param messages - Array of messages with role and content
505
624
  * @param options - Optional completion parameters
506
625
  * @returns ChatResponse object with choices and usage
507
626
  */
508
627
  async chatCompletion(model, messages, options) {
509
- const body = {
510
- model,
511
- messages,
512
- max_tokens: options?.maxTokens || DEFAULT_MAX_TOKENS
628
+ const buildBody = (m) => {
629
+ const body = {
630
+ model: m,
631
+ messages,
632
+ max_tokens: options?.maxTokens || DEFAULT_MAX_TOKENS
633
+ };
634
+ if (options?.temperature !== void 0) body.temperature = options.temperature;
635
+ if (options?.topP !== void 0) body.top_p = options.topP;
636
+ if (options?.searchParameters !== void 0) {
637
+ body.search_parameters = options.searchParameters;
638
+ } else if (options?.search === true) {
639
+ body.search_parameters = { mode: "on" };
640
+ }
641
+ if (options?.tools !== void 0) body.tools = options.tools;
642
+ if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
643
+ return body;
513
644
  };
514
- if (options?.temperature !== void 0) {
515
- body.temperature = options.temperature;
516
- }
517
- if (options?.topP !== void 0) {
518
- body.top_p = options.topP;
519
- }
520
- if (options?.searchParameters !== void 0) {
521
- body.search_parameters = options.searchParameters;
522
- } else if (options?.search === true) {
523
- body.search_parameters = { mode: "on" };
524
- }
525
- if (options?.tools !== void 0) {
526
- body.tools = options.tools;
645
+ const chain = [model, ...options?.fallbackModels ?? []];
646
+ let lastErr;
647
+ for (let i = 0; i < chain.length; i++) {
648
+ const candidate = chain[i];
649
+ try {
650
+ return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
651
+ } catch (err) {
652
+ lastErr = err;
653
+ const next = chain[i + 1];
654
+ if (!next || !isTransientError(err)) throw err;
655
+ console.error(
656
+ `[@blockrun/llm] ${candidate} -> ${next} (${errSummary(err)})`
657
+ );
658
+ }
527
659
  }
528
- if (options?.toolChoice !== void 0) {
529
- body.tool_choice = options.toolChoice;
660
+ throw lastErr;
661
+ }
662
+ /**
663
+ * Write a canonical cost_log entry after a settled x402 payment.
664
+ * Best-effort: failures here must never break a successful API call.
665
+ * Mirrors what Franklin's AgentClient writes via src/agent/llm.ts so
666
+ * cost_log.jsonl is a single source of truth regardless of caller.
667
+ */
668
+ recordCost(url, costUsd, opts) {
669
+ try {
670
+ let endpoint = "";
671
+ try {
672
+ endpoint = new URL(url).pathname;
673
+ } catch {
674
+ endpoint = "";
675
+ }
676
+ const model = opts?.body && typeof opts.body.model === "string" ? opts.body.model : void 0;
677
+ logCost({
678
+ ts: Date.now() / 1e3,
679
+ endpoint,
680
+ cost_usd: costUsd,
681
+ model,
682
+ wallet: this.account.address,
683
+ network: opts?.network,
684
+ client_kind: "LLMClient"
685
+ });
686
+ } catch {
530
687
  }
531
- return this.requestWithPayment("/v1/chat/completions", body);
532
688
  }
533
689
  /**
534
690
  * Make a request with automatic x402 payment handling.
@@ -651,6 +807,7 @@ var LLMClient = class _LLMClient {
651
807
  const costUsd2 = parseFloat(details.amount) / 1e6;
652
808
  this.sessionCalls += 1;
653
809
  this.sessionTotalUsd += costUsd2;
810
+ this.recordCost(url, costUsd2, { body, network: details.network });
654
811
  return retryResp2.json();
655
812
  }
656
813
  }
@@ -673,6 +830,7 @@ var LLMClient = class _LLMClient {
673
830
  const costUsd = parseFloat(details.amount) / 1e6;
674
831
  this.sessionCalls += 1;
675
832
  this.sessionTotalUsd += costUsd;
833
+ this.recordCost(url, costUsd, { body, network: details.network });
676
834
  return retryResponse.json();
677
835
  }
678
836
  /**
@@ -922,6 +1080,7 @@ var LLMClient = class _LLMClient {
922
1080
  const costUsd2 = parseFloat(details.amount) / 1e6;
923
1081
  this.sessionCalls += 1;
924
1082
  this.sessionTotalUsd += costUsd2;
1083
+ this.recordCost(url, costUsd2, { body, network: details.network });
925
1084
  return retryResp2.json();
926
1085
  }
927
1086
  }
@@ -944,6 +1103,7 @@ var LLMClient = class _LLMClient {
944
1103
  const costUsd = parseFloat(details.amount) / 1e6;
945
1104
  this.sessionCalls += 1;
946
1105
  this.sessionTotalUsd += costUsd;
1106
+ this.recordCost(url, costUsd, { body, network: details.network });
947
1107
  return retryResponse.json();
948
1108
  }
949
1109
  /**
@@ -1065,6 +1225,7 @@ var LLMClient = class _LLMClient {
1065
1225
  const costUsd2 = parseFloat(details.amount) / 1e6;
1066
1226
  this.sessionCalls += 1;
1067
1227
  this.sessionTotalUsd += costUsd2;
1228
+ this.recordCost(url, costUsd2, { network: details.network });
1068
1229
  return retryResp2.json();
1069
1230
  }
1070
1231
  }
@@ -1087,6 +1248,7 @@ var LLMClient = class _LLMClient {
1087
1248
  const costUsd = parseFloat(details.amount) / 1e6;
1088
1249
  this.sessionCalls += 1;
1089
1250
  this.sessionTotalUsd += costUsd;
1251
+ this.recordCost(url, costUsd, { network: details.network });
1090
1252
  return retryResponse.json();
1091
1253
  }
1092
1254
  /**
@@ -1106,9 +1268,59 @@ var LLMClient = class _LLMClient {
1106
1268
  }
1107
1269
  }
1108
1270
  /**
1109
- * List available LLM models with pricing.
1271
+ * List available models with pricing.
1272
+ *
1273
+ * Returns the full `/v1/models` unified catalog (chat + image + music).
1274
+ * The shape preserves backwards compatibility — image/music rows have
1275
+ * `inputPrice = outputPrice = 0` since those fields don't apply, and
1276
+ * their per-call price surfaces via `flatPrice`.
1110
1277
  */
1111
1278
  async listModels() {
1279
+ const raw = await this.fetchRawModels();
1280
+ return raw.map((m) => mapRawToModel(m));
1281
+ }
1282
+ /**
1283
+ * List available image generation models with pricing.
1284
+ *
1285
+ * The dedicated `/v1/images/models` endpoint was deprecated server-side;
1286
+ * image models live in the unified `/v1/models` catalog under
1287
+ * `categories: ["image", ...]`. This method filters that catalog so
1288
+ * existing callers keep working.
1289
+ */
1290
+ async listImageModels() {
1291
+ const raw = await this.fetchRawModels();
1292
+ return raw.filter((m) => Array.isArray(m.categories) && m.categories.includes("image")).map((m) => mapRawToImageModel(m));
1293
+ }
1294
+ /**
1295
+ * List all available models (chat, image, music, etc.) with pricing.
1296
+ *
1297
+ * @returns Array of all models with `type` field set from category
1298
+ * (`llm` for chat, `image` / `music` for media). Backwards-compat:
1299
+ * chat models always report `type: "llm"`.
1300
+ */
1301
+ async listAllModels() {
1302
+ const raw = await this.fetchRawModels();
1303
+ const out = [];
1304
+ for (const m of raw) {
1305
+ const cats = Array.isArray(m.categories) ? m.categories : [];
1306
+ if (cats.includes("image")) {
1307
+ const model = mapRawToImageModel(m);
1308
+ model.type = "image";
1309
+ out.push(model);
1310
+ } else {
1311
+ const model = mapRawToModel(m);
1312
+ model.type = "llm";
1313
+ out.push(model);
1314
+ }
1315
+ }
1316
+ return out;
1317
+ }
1318
+ /**
1319
+ * Internal: fetch the raw `/v1/models` catalog without normalising shape.
1320
+ * Used by listImageModels / listAllModels so each can pick category-
1321
+ * specific fields.
1322
+ */
1323
+ async fetchRawModels() {
1112
1324
  const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, {
1113
1325
  method: "GET"
1114
1326
  });
@@ -1126,65 +1338,8 @@ var LLMClient = class _LLMClient {
1126
1338
  );
1127
1339
  }
1128
1340
  const data = await response.json();
1129
- return (data.data || []).map((m) => ({
1130
- id: m.id,
1131
- name: m.name || m.id,
1132
- provider: m.provider || m.owned_by || "",
1133
- description: m.description || "",
1134
- inputPrice: m.inputPrice ?? m.input_price ?? m.pricing?.input ?? 0,
1135
- outputPrice: m.outputPrice ?? m.output_price ?? m.pricing?.output ?? 0,
1136
- contextWindow: m.contextWindow ?? m.context_window ?? 0,
1137
- maxOutput: m.maxOutput ?? m.max_output ?? 0,
1138
- categories: m.categories || [],
1139
- available: true,
1140
- billingMode: m.billingMode ?? m.billing_mode,
1141
- flatPrice: m.flatPrice ?? m.flat_price ?? m.pricing?.flat,
1142
- hidden: m.hidden
1143
- }));
1144
- }
1145
- /**
1146
- * List available image generation models with pricing.
1147
- */
1148
- async listImageModels() {
1149
- const response = await this.fetchWithTimeout(
1150
- `${this.apiUrl}/v1/images/models`,
1151
- { method: "GET" }
1152
- );
1153
- if (!response.ok) {
1154
- throw new APIError(
1155
- `Failed to list image models: ${response.status}`,
1156
- response.status
1157
- );
1158
- }
1159
- const data = await response.json();
1160
1341
  return data.data || [];
1161
1342
  }
1162
- /**
1163
- * List all available models (both LLM and image) with pricing.
1164
- *
1165
- * @returns Array of all models with 'type' field ('llm' or 'image')
1166
- *
1167
- * @example
1168
- * const models = await client.listAllModels();
1169
- * for (const model of models) {
1170
- * if (model.type === 'llm') {
1171
- * console.log(`LLM: ${model.id} - $${model.inputPrice}/M input`);
1172
- * } else {
1173
- * console.log(`Image: ${model.id} - $${model.pricePerImage}/image`);
1174
- * }
1175
- * }
1176
- */
1177
- async listAllModels() {
1178
- const llmModels = await this.listModels();
1179
- for (const model of llmModels) {
1180
- model.type = "llm";
1181
- }
1182
- const imageModels = await this.listImageModels();
1183
- for (const model of imageModels) {
1184
- model.type = "image";
1185
- }
1186
- return [...llmModels, ...imageModels];
1187
- }
1188
1343
  /**
1189
1344
  * Edit an image using img2img.
1190
1345
  *
@@ -1225,6 +1380,19 @@ var LLMClient = class _LLMClient {
1225
1380
  const data = await this.requestWithPaymentRaw("/v1/search", body);
1226
1381
  return data;
1227
1382
  }
1383
+ /**
1384
+ * Generic Exa endpoint proxy (POST). Useful when you need an Exa API
1385
+ * surface that the typed wrappers below don't expose.
1386
+ *
1387
+ * @param path - Exa endpoint segment: "search" | "find-similar" | "contents" | "answer"
1388
+ * @param body - Request body (see Exa API docs)
1389
+ *
1390
+ * @example
1391
+ * const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
1392
+ */
1393
+ async exa(path5, body) {
1394
+ return this.requestWithPaymentRaw(`/v1/exa/${path5}`, body);
1395
+ }
1228
1396
  /**
1229
1397
  * Neural web search via Exa. Returns semantically relevant URLs and metadata.
1230
1398
  * Understands meaning, not just keywords. $0.01/call.
@@ -1278,9 +1446,7 @@ var LLMClient = class _LLMClient {
1278
1446
  return data.data;
1279
1447
  }
1280
1448
  /**
1281
- * Get USDC balance on Base network.
1282
- *
1283
- * Automatically detects mainnet vs testnet based on API URL.
1449
+ * Get USDC balance on Base mainnet.
1284
1450
  *
1285
1451
  * @returns USDC balance as a float (6 decimal places normalized)
1286
1452
  *
@@ -1289,9 +1455,8 @@ var LLMClient = class _LLMClient {
1289
1455
  * console.log(`Balance: $${balance.toFixed(2)} USDC`);
1290
1456
  */
1291
1457
  async getBalance() {
1292
- const isTestnet = this.isTestnet();
1293
- const usdcContract = isTestnet ? "0x036CbD53842c5426634e7929541eC2318f3dCF7e" : "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1294
- const rpcs = isTestnet ? ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"] : ["https://base.publicnode.com", "https://mainnet.base.org", "https://base.meowrpc.com"];
1458
+ const usdcContract = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1459
+ const rpcs = ["https://base.publicnode.com", "https://mainnet.base.org", "https://base.meowrpc.com"];
1295
1460
  const selector = "0x70a08231";
1296
1461
  const paddedAddress = this.account.address.slice(2).toLowerCase().padStart(64, "0");
1297
1462
  const data = selector + paddedAddress;
@@ -1622,19 +1787,7 @@ var LLMClient = class _LLMClient {
1622
1787
  getWalletAddress() {
1623
1788
  return this.account.address;
1624
1789
  }
1625
- /**
1626
- * Check if client is configured for testnet.
1627
- */
1628
- isTestnet() {
1629
- return this.apiUrl.includes("testnet.blockrun.ai");
1630
- }
1631
1790
  };
1632
- function testnetClient(options = {}) {
1633
- return new LLMClient({
1634
- ...options,
1635
- apiUrl: TESTNET_API_URL
1636
- });
1637
- }
1638
1791
  var client_default = LLMClient;
1639
1792
 
1640
1793
  // src/image.ts
@@ -1723,10 +1876,15 @@ var ImageClient = class {
1723
1876
  }
1724
1877
  /**
1725
1878
  * List available image generation models with pricing.
1879
+ *
1880
+ * The dedicated `/v1/images/models` endpoint was deprecated server-side;
1881
+ * image models live in the unified `/v1/models` catalog under
1882
+ * `categories: ["image", ...]`. This method filters that catalog so
1883
+ * existing callers keep working.
1726
1884
  */
1727
1885
  async listImageModels() {
1728
1886
  const response = await this.fetchWithTimeout(
1729
- `${this.apiUrl}/v1/images/models`,
1887
+ `${this.apiUrl}/v1/models`,
1730
1888
  { method: "GET" }
1731
1889
  );
1732
1890
  if (!response.ok) {
@@ -1736,7 +1894,16 @@ var ImageClient = class {
1736
1894
  );
1737
1895
  }
1738
1896
  const data = await response.json();
1739
- return data.data || [];
1897
+ return (data.data || []).filter((m) => Array.isArray(m.categories) && m.categories.includes("image")).map((m) => ({
1898
+ id: m.id,
1899
+ name: m.name || m.id,
1900
+ provider: m.provider || m.owned_by || "",
1901
+ description: m.description || "",
1902
+ pricePerImage: m.pricePerImage ?? m.price_per_image ?? m.pricing?.flat ?? m.flatPrice ?? m.flat_price ?? 0,
1903
+ supportedSizes: m.supportedSizes ?? m.supported_sizes,
1904
+ maxPromptLength: m.maxPromptLength ?? m.max_prompt_length,
1905
+ available: true
1906
+ }));
1740
1907
  }
1741
1908
  /**
1742
1909
  * Make a request with automatic x402 payment handling.
@@ -2631,13 +2798,13 @@ function buildUrl(base, query) {
2631
2798
 
2632
2799
  // src/wallet.ts
2633
2800
  import { privateKeyToAccount as privateKeyToAccount7, generatePrivateKey } from "viem/accounts";
2634
- import * as fs from "fs";
2635
- import * as path from "path";
2636
- import * as os from "os";
2801
+ import * as fs2 from "fs";
2802
+ import * as path2 from "path";
2803
+ import * as os2 from "os";
2637
2804
  var USDC_BASE_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
2638
2805
  var BASE_CHAIN_ID2 = "8453";
2639
- var WALLET_DIR = path.join(os.homedir(), ".blockrun");
2640
- var WALLET_FILE = path.join(WALLET_DIR, ".session");
2806
+ var WALLET_DIR = path2.join(os2.homedir(), ".blockrun");
2807
+ var WALLET_FILE = path2.join(WALLET_DIR, ".session");
2641
2808
  function createWallet() {
2642
2809
  const privateKey = generatePrivateKey();
2643
2810
  const account = privateKeyToAccount7(privateKey);
@@ -2647,27 +2814,27 @@ function createWallet() {
2647
2814
  };
2648
2815
  }
2649
2816
  function saveWallet(privateKey) {
2650
- if (!fs.existsSync(WALLET_DIR)) {
2651
- fs.mkdirSync(WALLET_DIR, { recursive: true });
2817
+ if (!fs2.existsSync(WALLET_DIR)) {
2818
+ fs2.mkdirSync(WALLET_DIR, { recursive: true });
2652
2819
  }
2653
- fs.writeFileSync(WALLET_FILE, privateKey, { mode: 384 });
2820
+ fs2.writeFileSync(WALLET_FILE, privateKey, { mode: 384 });
2654
2821
  return WALLET_FILE;
2655
2822
  }
2656
2823
  function scanWallets() {
2657
- const home = os.homedir();
2824
+ const home = os2.homedir();
2658
2825
  const results = [];
2659
2826
  try {
2660
- const entries = fs.readdirSync(home, { withFileTypes: true });
2827
+ const entries = fs2.readdirSync(home, { withFileTypes: true });
2661
2828
  for (const entry of entries) {
2662
2829
  if (!entry.name.startsWith(".") || !entry.isDirectory()) continue;
2663
- const walletFile = path.join(home, entry.name, "wallet.json");
2664
- if (!fs.existsSync(walletFile)) continue;
2830
+ const walletFile = path2.join(home, entry.name, "wallet.json");
2831
+ if (!fs2.existsSync(walletFile)) continue;
2665
2832
  try {
2666
- const data = JSON.parse(fs.readFileSync(walletFile, "utf-8"));
2833
+ const data = JSON.parse(fs2.readFileSync(walletFile, "utf-8"));
2667
2834
  const pk = data.privateKey || "";
2668
2835
  const addr = data.address || "";
2669
2836
  if (pk && addr) {
2670
- const mtime = fs.statSync(walletFile).mtimeMs;
2837
+ const mtime = fs2.statSync(walletFile).mtimeMs;
2671
2838
  results.push({ mtime, privateKey: pk, address: addr });
2672
2839
  }
2673
2840
  } catch {
@@ -2682,13 +2849,13 @@ function scanWallets() {
2682
2849
  function loadWallet() {
2683
2850
  const wallets = scanWallets();
2684
2851
  if (wallets.length > 0) return wallets[0].privateKey;
2685
- if (fs.existsSync(WALLET_FILE)) {
2686
- const key = fs.readFileSync(WALLET_FILE, "utf-8").trim();
2852
+ if (fs2.existsSync(WALLET_FILE)) {
2853
+ const key = fs2.readFileSync(WALLET_FILE, "utf-8").trim();
2687
2854
  if (key) return key;
2688
2855
  }
2689
- const legacyFile = path.join(WALLET_DIR, "wallet.key");
2690
- if (fs.existsSync(legacyFile)) {
2691
- const key = fs.readFileSync(legacyFile, "utf-8").trim();
2856
+ const legacyFile = path2.join(WALLET_DIR, "wallet.key");
2857
+ if (fs2.existsSync(legacyFile)) {
2858
+ const key = fs2.readFileSync(legacyFile, "utf-8").trim();
2692
2859
  if (key) return key;
2693
2860
  }
2694
2861
  return null;
@@ -2783,11 +2950,11 @@ var WALLET_FILE_PATH = WALLET_FILE;
2783
2950
  var WALLET_DIR_PATH = WALLET_DIR;
2784
2951
 
2785
2952
  // src/solana-wallet.ts
2786
- import * as fs2 from "fs";
2787
- import * as path2 from "path";
2788
- import * as os2 from "os";
2789
- var WALLET_DIR2 = path2.join(os2.homedir(), ".blockrun");
2790
- var SOLANA_WALLET_FILE = path2.join(WALLET_DIR2, ".solana-session");
2953
+ import * as fs3 from "fs";
2954
+ import * as path3 from "path";
2955
+ import * as os3 from "os";
2956
+ var WALLET_DIR2 = path3.join(os3.homedir(), ".blockrun");
2957
+ var SOLANA_WALLET_FILE = path3.join(WALLET_DIR2, ".solana-session");
2791
2958
  async function createSolanaWallet() {
2792
2959
  const { Keypair } = await import("@solana/web3.js");
2793
2960
  const bs58 = await import("bs58");
@@ -2816,39 +2983,39 @@ async function solanaPublicKey(privateKey) {
2816
2983
  return Keypair.fromSecretKey(bytes).publicKey.toBase58();
2817
2984
  }
2818
2985
  function saveSolanaWallet(privateKey) {
2819
- if (!fs2.existsSync(WALLET_DIR2)) fs2.mkdirSync(WALLET_DIR2, { recursive: true });
2820
- fs2.writeFileSync(SOLANA_WALLET_FILE, privateKey, { mode: 384 });
2986
+ if (!fs3.existsSync(WALLET_DIR2)) fs3.mkdirSync(WALLET_DIR2, { recursive: true });
2987
+ fs3.writeFileSync(SOLANA_WALLET_FILE, privateKey, { mode: 384 });
2821
2988
  return SOLANA_WALLET_FILE;
2822
2989
  }
2823
2990
  function scanSolanaWallets() {
2824
- const home = os2.homedir();
2991
+ const home = os3.homedir();
2825
2992
  const results = [];
2826
2993
  try {
2827
- const entries = fs2.readdirSync(home, { withFileTypes: true });
2994
+ const entries = fs3.readdirSync(home, { withFileTypes: true });
2828
2995
  for (const entry of entries) {
2829
2996
  if (!entry.name.startsWith(".") || !entry.isDirectory()) continue;
2830
- const solanaWalletFile = path2.join(home, entry.name, "solana-wallet.json");
2831
- if (fs2.existsSync(solanaWalletFile)) {
2997
+ const solanaWalletFile = path3.join(home, entry.name, "solana-wallet.json");
2998
+ if (fs3.existsSync(solanaWalletFile)) {
2832
2999
  try {
2833
- const data = JSON.parse(fs2.readFileSync(solanaWalletFile, "utf-8"));
3000
+ const data = JSON.parse(fs3.readFileSync(solanaWalletFile, "utf-8"));
2834
3001
  const pk = data.privateKey || "";
2835
3002
  const addr = data.address || "";
2836
3003
  if (pk && addr) {
2837
- const mtime = fs2.statSync(solanaWalletFile).mtimeMs;
3004
+ const mtime = fs3.statSync(solanaWalletFile).mtimeMs;
2838
3005
  results.push({ mtime, secretKey: pk, publicKey: addr });
2839
3006
  }
2840
3007
  } catch {
2841
3008
  }
2842
3009
  }
2843
3010
  if (entry.name === ".brcc") {
2844
- const brccWalletFile = path2.join(home, entry.name, "wallet.json");
2845
- if (fs2.existsSync(brccWalletFile)) {
3011
+ const brccWalletFile = path3.join(home, entry.name, "wallet.json");
3012
+ if (fs3.existsSync(brccWalletFile)) {
2846
3013
  try {
2847
- const data = JSON.parse(fs2.readFileSync(brccWalletFile, "utf-8"));
3014
+ const data = JSON.parse(fs3.readFileSync(brccWalletFile, "utf-8"));
2848
3015
  const pk = data.privateKey || "";
2849
3016
  const addr = data.address || "";
2850
3017
  if (pk && addr) {
2851
- const mtime = fs2.statSync(brccWalletFile).mtimeMs;
3018
+ const mtime = fs3.statSync(brccWalletFile).mtimeMs;
2852
3019
  results.push({ mtime, secretKey: pk, publicKey: addr });
2853
3020
  }
2854
3021
  } catch {
@@ -2864,8 +3031,8 @@ function scanSolanaWallets() {
2864
3031
  function loadSolanaWallet() {
2865
3032
  const wallets = scanSolanaWallets();
2866
3033
  if (wallets.length > 0) return wallets[0].secretKey;
2867
- if (fs2.existsSync(SOLANA_WALLET_FILE)) {
2868
- const key = fs2.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
3034
+ if (fs3.existsSync(SOLANA_WALLET_FILE)) {
3035
+ const key = fs3.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
2869
3036
  if (key) return key;
2870
3037
  }
2871
3038
  return null;
@@ -2880,8 +3047,8 @@ async function getOrCreateSolanaWallet() {
2880
3047
  if (wallets.length > 0) {
2881
3048
  return { privateKey: wallets[0].secretKey, address: wallets[0].publicKey, isNew: false };
2882
3049
  }
2883
- if (fs2.existsSync(SOLANA_WALLET_FILE)) {
2884
- const fileKey = fs2.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
3050
+ if (fs3.existsSync(SOLANA_WALLET_FILE)) {
3051
+ const fileKey = fs3.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
2885
3052
  if (fileKey) {
2886
3053
  const address2 = await solanaPublicKey(fileKey);
2887
3054
  return { privateKey: fileKey, address: address2, isNew: false };
@@ -3455,13 +3622,13 @@ function solanaClient(options = {}) {
3455
3622
  }
3456
3623
 
3457
3624
  // src/cache.ts
3458
- import * as fs3 from "fs";
3459
- import * as path3 from "path";
3460
- import * as os3 from "os";
3625
+ import * as fs4 from "fs";
3626
+ import * as path4 from "path";
3627
+ import * as os4 from "os";
3461
3628
  import * as crypto2 from "crypto";
3462
- var CACHE_DIR = path3.join(os3.homedir(), ".blockrun", "cache");
3463
- var DATA_DIR = path3.join(os3.homedir(), ".blockrun", "data");
3464
- var COST_LOG_FILE = path3.join(os3.homedir(), ".blockrun", "cost_log.jsonl");
3629
+ var CACHE_DIR = path4.join(os4.homedir(), ".blockrun", "cache");
3630
+ var DATA_DIR = path4.join(os4.homedir(), ".blockrun", "data");
3631
+ var COST_LOG_FILE2 = path4.join(os4.homedir(), ".blockrun", "cost_log.jsonl");
3465
3632
  var DEFAULT_TTL = {
3466
3633
  "/v1/x/": 3600 * 1e3,
3467
3634
  "/v1/partner/": 3600 * 1e3,
@@ -3482,19 +3649,19 @@ function cacheKey(endpoint, body) {
3482
3649
  return crypto2.createHash("sha256").update(keyData).digest("hex").slice(0, 16);
3483
3650
  }
3484
3651
  function cachePath(key) {
3485
- return path3.join(CACHE_DIR, `${key}.json`);
3652
+ return path4.join(CACHE_DIR, `${key}.json`);
3486
3653
  }
3487
3654
  function getCached(key) {
3488
3655
  const filePath = cachePath(key);
3489
- if (!fs3.existsSync(filePath)) return null;
3656
+ if (!fs4.existsSync(filePath)) return null;
3490
3657
  try {
3491
- const raw = fs3.readFileSync(filePath, "utf-8");
3658
+ const raw = fs4.readFileSync(filePath, "utf-8");
3492
3659
  const entry = JSON.parse(raw);
3493
3660
  const ttl = entry.ttlMs ?? getTtl(entry.endpoint ?? "");
3494
3661
  if (ttl <= 0) return null;
3495
3662
  if (Date.now() - entry.cachedAt > ttl) {
3496
3663
  try {
3497
- fs3.unlinkSync(filePath);
3664
+ fs4.unlinkSync(filePath);
3498
3665
  } catch {
3499
3666
  }
3500
3667
  return null;
@@ -3513,7 +3680,7 @@ function getCachedByRequest(endpoint, body) {
3513
3680
  function setCache(key, data, ttlMs) {
3514
3681
  if (ttlMs <= 0) return;
3515
3682
  try {
3516
- fs3.mkdirSync(CACHE_DIR, { recursive: true });
3683
+ fs4.mkdirSync(CACHE_DIR, { recursive: true });
3517
3684
  } catch {
3518
3685
  }
3519
3686
  const entry = {
@@ -3522,7 +3689,7 @@ function setCache(key, data, ttlMs) {
3522
3689
  ttlMs
3523
3690
  };
3524
3691
  try {
3525
- fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
3692
+ fs4.writeFileSync(cachePath(key), JSON.stringify(entry));
3526
3693
  } catch {
3527
3694
  }
3528
3695
  }
@@ -3545,7 +3712,7 @@ function readableFilename(endpoint, body) {
3545
3712
  }
3546
3713
  function saveReadable(endpoint, body, response, costUsd) {
3547
3714
  try {
3548
- fs3.mkdirSync(DATA_DIR, { recursive: true });
3715
+ fs4.mkdirSync(DATA_DIR, { recursive: true });
3549
3716
  } catch {
3550
3717
  }
3551
3718
  const filename = readableFilename(endpoint, body);
@@ -3557,14 +3724,14 @@ function saveReadable(endpoint, body, response, costUsd) {
3557
3724
  response
3558
3725
  };
3559
3726
  try {
3560
- fs3.writeFileSync(path3.join(DATA_DIR, filename), JSON.stringify(entry, null, 2));
3727
+ fs4.writeFileSync(path4.join(DATA_DIR, filename), JSON.stringify(entry, null, 2));
3561
3728
  } catch {
3562
3729
  }
3563
3730
  }
3564
3731
  function appendCostLog(endpoint, costUsd) {
3565
3732
  if (costUsd <= 0) return;
3566
3733
  try {
3567
- fs3.mkdirSync(path3.dirname(COST_LOG_FILE), { recursive: true });
3734
+ fs4.mkdirSync(path4.dirname(COST_LOG_FILE2), { recursive: true });
3568
3735
  } catch {
3569
3736
  }
3570
3737
  const entry = {
@@ -3573,7 +3740,7 @@ function appendCostLog(endpoint, costUsd) {
3573
3740
  cost_usd: costUsd
3574
3741
  };
3575
3742
  try {
3576
- fs3.appendFileSync(COST_LOG_FILE, JSON.stringify(entry) + "\n");
3743
+ fs4.appendFileSync(COST_LOG_FILE2, JSON.stringify(entry) + "\n");
3577
3744
  } catch {
3578
3745
  }
3579
3746
  }
@@ -3581,7 +3748,7 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
3581
3748
  const ttl = getTtl(endpoint);
3582
3749
  if (ttl > 0) {
3583
3750
  try {
3584
- fs3.mkdirSync(CACHE_DIR, { recursive: true });
3751
+ fs4.mkdirSync(CACHE_DIR, { recursive: true });
3585
3752
  } catch {
3586
3753
  }
3587
3754
  const key = cacheKey(endpoint, body);
@@ -3593,7 +3760,7 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
3593
3760
  costUsd
3594
3761
  };
3595
3762
  try {
3596
- fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
3763
+ fs4.writeFileSync(cachePath(key), JSON.stringify(entry));
3597
3764
  } catch {
3598
3765
  }
3599
3766
  }
@@ -3601,14 +3768,14 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
3601
3768
  appendCostLog(endpoint, costUsd);
3602
3769
  }
3603
3770
  function clearCache() {
3604
- if (!fs3.existsSync(CACHE_DIR)) return 0;
3771
+ if (!fs4.existsSync(CACHE_DIR)) return 0;
3605
3772
  let count = 0;
3606
3773
  try {
3607
- const files = fs3.readdirSync(CACHE_DIR);
3774
+ const files = fs4.readdirSync(CACHE_DIR);
3608
3775
  for (const file of files) {
3609
3776
  if (file.endsWith(".json")) {
3610
3777
  try {
3611
- fs3.unlinkSync(path3.join(CACHE_DIR, file));
3778
+ fs4.unlinkSync(path4.join(CACHE_DIR, file));
3612
3779
  count++;
3613
3780
  } catch {
3614
3781
  }
@@ -3619,14 +3786,14 @@ function clearCache() {
3619
3786
  return count;
3620
3787
  }
3621
3788
  function getCostLogSummary() {
3622
- if (!fs3.existsSync(COST_LOG_FILE)) {
3789
+ if (!fs4.existsSync(COST_LOG_FILE2)) {
3623
3790
  return { totalUsd: 0, calls: 0, byEndpoint: {} };
3624
3791
  }
3625
3792
  let totalUsd = 0;
3626
3793
  let calls = 0;
3627
3794
  const byEndpoint = {};
3628
3795
  try {
3629
- const content = fs3.readFileSync(COST_LOG_FILE, "utf-8").trim();
3796
+ const content = fs4.readFileSync(COST_LOG_FILE2, "utf-8").trim();
3630
3797
  if (!content) return { totalUsd: 0, calls: 0, byEndpoint: {} };
3631
3798
  for (const line of content.split("\n")) {
3632
3799
  if (!line) continue;
@@ -3681,47 +3848,6 @@ async function status() {
3681
3848
  return { address, balance };
3682
3849
  }
3683
3850
 
3684
- // src/cost-log.ts
3685
- import * as fs4 from "fs";
3686
- import * as path4 from "path";
3687
- import * as os4 from "os";
3688
- var DATA_DIR2 = path4.join(os4.homedir(), ".blockrun", "data");
3689
- var COST_LOG_FILE2 = path4.join(DATA_DIR2, "costs.jsonl");
3690
- function logCost(entry) {
3691
- try {
3692
- fs4.mkdirSync(DATA_DIR2, { recursive: true });
3693
- } catch {
3694
- }
3695
- try {
3696
- fs4.appendFileSync(COST_LOG_FILE2, JSON.stringify(entry) + "\n");
3697
- } catch {
3698
- }
3699
- }
3700
- function getCostSummary() {
3701
- if (!fs4.existsSync(COST_LOG_FILE2)) {
3702
- return { totalUsd: 0, calls: 0, byModel: {} };
3703
- }
3704
- let totalUsd = 0;
3705
- let calls = 0;
3706
- const byModel = {};
3707
- try {
3708
- const content = fs4.readFileSync(COST_LOG_FILE2, "utf-8").trim();
3709
- if (!content) return { totalUsd: 0, calls: 0, byModel: {} };
3710
- for (const line of content.split("\n")) {
3711
- if (!line) continue;
3712
- try {
3713
- const entry = JSON.parse(line);
3714
- totalUsd += entry.costUsd;
3715
- calls += 1;
3716
- byModel[entry.model] = (byModel[entry.model] || 0) + entry.costUsd;
3717
- } catch {
3718
- }
3719
- }
3720
- } catch {
3721
- }
3722
- return { totalUsd, calls, byModel };
3723
- }
3724
-
3725
3851
  // src/openai-compat.ts
3726
3852
  var StreamingResponse = class {
3727
3853
  reader;
@@ -4029,7 +4155,6 @@ export {
4029
4155
  solanaKeyToBytes,
4030
4156
  solanaPublicKey,
4031
4157
  status,
4032
- testnetClient,
4033
4158
  validateMaxTokens,
4034
4159
  validateModel,
4035
4160
  validateTemperature,