@blockrun/llm 1.15.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -86,7 +86,6 @@ __export(index_exports, {
86
86
  solanaKeyToBytes: () => solanaKeyToBytes,
87
87
  solanaPublicKey: () => solanaPublicKey,
88
88
  status: () => status,
89
- testnetClient: () => testnetClient,
90
89
  validateMaxTokens: () => validateMaxTokens,
91
90
  validateModel: () => validateModel,
92
91
  validateTemperature: () => validateTemperature,
@@ -437,16 +436,107 @@ function validateResourceUrl(url, baseUrl) {
437
436
  }
438
437
  }
439
438
 
439
+ // src/cost-log.ts
440
+ var fs = __toESM(require("fs"), 1);
441
+ var path = __toESM(require("path"), 1);
442
+ var os = __toESM(require("os"), 1);
443
+ var BLOCKRUN_DIR = path.join(os.homedir(), ".blockrun");
444
+ var COST_LOG_FILE = path.join(BLOCKRUN_DIR, "cost_log.jsonl");
445
+ function logCost(entry) {
446
+ try {
447
+ fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
448
+ } catch {
449
+ }
450
+ try {
451
+ fs.appendFileSync(COST_LOG_FILE, JSON.stringify(entry) + "\n");
452
+ } catch {
453
+ }
454
+ }
455
+ function getCostSummary() {
456
+ if (!fs.existsSync(COST_LOG_FILE)) {
457
+ return { totalUsd: 0, calls: 0, byModel: {}, byEndpoint: {} };
458
+ }
459
+ let totalUsd = 0;
460
+ let calls = 0;
461
+ const byModel = {};
462
+ const byEndpoint = {};
463
+ try {
464
+ const content = fs.readFileSync(COST_LOG_FILE, "utf-8").trim();
465
+ if (!content) return { totalUsd: 0, calls: 0, byModel: {}, byEndpoint: {} };
466
+ for (const line of content.split("\n")) {
467
+ if (!line) continue;
468
+ try {
469
+ const raw = JSON.parse(line);
470
+ const cost = typeof raw.cost_usd === "number" ? raw.cost_usd : raw.costUsd ?? 0;
471
+ if (!cost) continue;
472
+ totalUsd += cost;
473
+ calls += 1;
474
+ if (raw.model) byModel[raw.model] = (byModel[raw.model] || 0) + cost;
475
+ if (raw.endpoint) byEndpoint[raw.endpoint] = (byEndpoint[raw.endpoint] || 0) + cost;
476
+ } catch {
477
+ }
478
+ }
479
+ } catch {
480
+ }
481
+ return { totalUsd, calls, byModel, byEndpoint };
482
+ }
483
+
440
484
  // src/client.ts
485
+ function isTransientError(err) {
486
+ if (err instanceof PaymentError) return false;
487
+ if (err instanceof APIError) {
488
+ return [502, 503, 504, 522, 524].includes(err.statusCode);
489
+ }
490
+ if (err instanceof Error) {
491
+ if (err.name === "AbortError") return true;
492
+ if (err.name === "TypeError" && /fetch|network/i.test(err.message)) return true;
493
+ }
494
+ return false;
495
+ }
496
+ function errSummary(err) {
497
+ if (err instanceof APIError) return `APIError ${err.statusCode}`;
498
+ if (err instanceof Error) {
499
+ const msg = err.message.length > 80 ? err.message.slice(0, 80) : err.message;
500
+ return `${err.name}: ${msg}`;
501
+ }
502
+ return String(err).slice(0, 100);
503
+ }
504
+ function mapRawToModel(m) {
505
+ return {
506
+ id: m.id,
507
+ name: m.name || m.id,
508
+ provider: m.provider || m.owned_by || "",
509
+ description: m.description || "",
510
+ inputPrice: m.inputPrice ?? m.input_price ?? m.pricing?.input ?? 0,
511
+ outputPrice: m.outputPrice ?? m.output_price ?? m.pricing?.output ?? 0,
512
+ contextWindow: m.contextWindow ?? m.context_window ?? 0,
513
+ maxOutput: m.maxOutput ?? m.max_output ?? 0,
514
+ categories: m.categories || [],
515
+ available: true,
516
+ billingMode: m.billingMode ?? m.billing_mode,
517
+ flatPrice: m.flatPrice ?? m.flat_price ?? m.pricing?.flat,
518
+ hidden: m.hidden
519
+ };
520
+ }
521
+ function mapRawToImageModel(m) {
522
+ return {
523
+ id: m.id,
524
+ name: m.name || m.id,
525
+ provider: m.provider || m.owned_by || "",
526
+ description: m.description || "",
527
+ pricePerImage: m.pricePerImage ?? m.price_per_image ?? m.pricing?.flat ?? m.flatPrice ?? m.flat_price ?? 0,
528
+ supportedSizes: m.supportedSizes ?? m.supported_sizes,
529
+ maxPromptLength: m.maxPromptLength ?? m.max_prompt_length,
530
+ available: true
531
+ };
532
+ }
441
533
  var DEFAULT_API_URL = "https://blockrun.ai/api";
442
- var TESTNET_API_URL = "https://testnet.blockrun.ai/api";
443
534
  var DEFAULT_MAX_TOKENS = 1024;
444
535
  var DEFAULT_TIMEOUT = 6e4;
445
536
  var SDK_VERSION = "1.5.0";
446
537
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
447
538
  var LLMClient = class _LLMClient {
448
539
  static DEFAULT_API_URL = DEFAULT_API_URL;
449
- static TESTNET_API_URL = TESTNET_API_URL;
450
540
  account;
451
541
  privateKey;
452
542
  apiUrl;
@@ -504,7 +594,8 @@ var LLMClient = class _LLMClient {
504
594
  temperature: options?.temperature,
505
595
  topP: options?.topP,
506
596
  search: options?.search,
507
- searchParameters: options?.searchParameters
597
+ searchParameters: options?.searchParameters,
598
+ fallbackModels: options?.fallbackModels
508
599
  });
509
600
  return result.choices[0].message.content || "";
510
601
  }
@@ -546,18 +637,24 @@ var LLMClient = class _LLMClient {
546
637
  modelPricing,
547
638
  routingProfile: options?.routingProfile
548
639
  });
640
+ const tierConfigs = decision.tierConfigs ?? import_clawrouter.DEFAULT_ROUTING_CONFIG.tiers;
641
+ const fullChain = (0, import_clawrouter.getFallbackChain)(decision.tier, tierConfigs);
642
+ const fallbacks = fullChain.filter(
643
+ (id) => id !== decision.model && modelPricing.has(id)
644
+ );
549
645
  const response = await this.chat(decision.model, prompt, {
550
646
  system: options?.system,
551
647
  maxTokens: options?.maxTokens,
552
648
  temperature: options?.temperature,
553
649
  topP: options?.topP,
554
650
  search: options?.search,
555
- searchParameters: options?.searchParameters
651
+ searchParameters: options?.searchParameters,
652
+ fallbackModels: fallbacks
556
653
  });
557
654
  return {
558
655
  response,
559
656
  model: decision.model,
560
- routing: decision
657
+ routing: { ...decision, fallbacks }
561
658
  };
562
659
  }
563
660
  /**
@@ -581,50 +678,130 @@ var LLMClient = class _LLMClient {
581
678
  }
582
679
  /**
583
680
  * Fetch model pricing from API.
681
+ *
682
+ * For flat-billed models (e.g. ZAI GLM-5 family at $0.001/call) the
683
+ * router still expects per-token rates, so we synthesise an equivalent
684
+ * per-token price assuming ~1500 total tokens per call. Without this,
685
+ * flat models would resolve to inputPrice=outputPrice=0 and the router
686
+ * would treat them as free, biasing routing decisions and reporting
687
+ * inflated savings %.
584
688
  */
585
689
  async fetchModelPricing() {
586
690
  const models = await this.listModels();
587
691
  const pricing = /* @__PURE__ */ new Map();
588
692
  for (const model of models) {
589
- pricing.set(model.id, {
590
- inputPrice: model.inputPrice,
591
- outputPrice: model.outputPrice
592
- });
693
+ if (model.billingMode === "flat" && model.flatPrice && model.flatPrice > 0) {
694
+ const perDirection = model.flatPrice * 1e6 / 1500 / 2;
695
+ pricing.set(model.id, {
696
+ inputPrice: perDirection,
697
+ outputPrice: perDirection
698
+ });
699
+ } else {
700
+ pricing.set(model.id, {
701
+ inputPrice: model.inputPrice,
702
+ outputPrice: model.outputPrice
703
+ });
704
+ }
593
705
  }
594
706
  return pricing;
595
707
  }
596
708
  /**
597
709
  * Full chat completion interface (OpenAI-compatible).
598
710
  *
599
- * @param model - Model ID
711
+ * When `fallbackModels` is set, transient failures (timeouts, network
712
+ * errors, 5xx) on the primary model trigger a retry against the next
713
+ * model in the list before raising. 4xx errors and PaymentError
714
+ * propagate immediately — those aren't "swap upstream and retry"
715
+ * situations. Each fallback hop logs one stderr line.
716
+ *
717
+ * @param model - Primary model ID
600
718
  * @param messages - Array of messages with role and content
601
719
  * @param options - Optional completion parameters
602
720
  * @returns ChatResponse object with choices and usage
603
721
  */
604
722
  async chatCompletion(model, messages, options) {
605
- const body = {
606
- model,
607
- messages,
608
- max_tokens: options?.maxTokens || DEFAULT_MAX_TOKENS
723
+ const buildBody = (m) => {
724
+ const body = {
725
+ model: m,
726
+ messages,
727
+ max_tokens: options?.maxTokens || DEFAULT_MAX_TOKENS
728
+ };
729
+ if (options?.temperature !== void 0) body.temperature = options.temperature;
730
+ if (options?.topP !== void 0) body.top_p = options.topP;
731
+ if (options?.searchParameters !== void 0) {
732
+ body.search_parameters = options.searchParameters;
733
+ } else if (options?.search === true) {
734
+ body.search_parameters = { mode: "on" };
735
+ }
736
+ if (options?.tools !== void 0) body.tools = options.tools;
737
+ if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
738
+ return body;
609
739
  };
610
- if (options?.temperature !== void 0) {
611
- body.temperature = options.temperature;
612
- }
613
- if (options?.topP !== void 0) {
614
- body.top_p = options.topP;
615
- }
616
- if (options?.searchParameters !== void 0) {
617
- body.search_parameters = options.searchParameters;
618
- } else if (options?.search === true) {
619
- body.search_parameters = { mode: "on" };
620
- }
621
- if (options?.tools !== void 0) {
622
- body.tools = options.tools;
740
+ const chain = [model, ...options?.fallbackModels ?? []];
741
+ let lastErr;
742
+ for (let i = 0; i < chain.length; i++) {
743
+ const candidate = chain[i];
744
+ try {
745
+ return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
746
+ } catch (err) {
747
+ lastErr = err;
748
+ const next = chain[i + 1];
749
+ if (!next || !isTransientError(err)) throw err;
750
+ console.error(
751
+ `[@blockrun/llm] ${candidate} -> ${next} (${errSummary(err)})`
752
+ );
753
+ }
623
754
  }
624
- if (options?.toolChoice !== void 0) {
625
- body.tool_choice = options.toolChoice;
755
+ throw lastErr;
756
+ }
757
+ /**
758
+ * Write a canonical cost_log entry after a settled x402 payment.
759
+ * Best-effort: failures here must never break a successful API call.
760
+ * Mirrors what Franklin's AgentClient writes via src/agent/llm.ts so
761
+ * cost_log.jsonl is a single source of truth regardless of caller.
762
+ */
763
+ recordCost(url, costUsd, opts) {
764
+ try {
765
+ let endpoint = "";
766
+ try {
767
+ endpoint = new URL(url).pathname;
768
+ } catch {
769
+ endpoint = "";
770
+ }
771
+ const model = opts?.body && typeof opts.body.model === "string" ? opts.body.model : void 0;
772
+ logCost({
773
+ ts: Date.now() / 1e3,
774
+ endpoint,
775
+ cost_usd: costUsd,
776
+ model,
777
+ wallet: this.account.address,
778
+ network: opts?.network,
779
+ client_kind: "LLMClient"
780
+ });
781
+ } catch {
626
782
  }
627
- return this.requestWithPayment("/v1/chat/completions", body);
783
+ }
784
+ /**
785
+ * Parse the chat response JSON and attach `fallback` metadata when the
786
+ * gateway signalled a transparent free-fallback substitution. The
787
+ * gateway sets X-Fallback-Used / X-Fallback-Model / X-Settlement-Skipped
788
+ * on the response when it served a paid request from a free model
789
+ * (route.ts createPaymentResponseHeader path). Without surfacing these
790
+ * to the caller, the user gets a different model than requested with
791
+ * no visibility — silent quality drop and no clue why the on-chain
792
+ * balance didn't change.
793
+ */
794
+ async parseChatResponse(response) {
795
+ const body = await response.json();
796
+ const used = response.headers.get("X-Fallback-Used") === "true";
797
+ if (used) {
798
+ body.fallback = {
799
+ used: true,
800
+ model: response.headers.get("X-Fallback-Model") || void 0,
801
+ settlementSkipped: response.headers.get("X-Settlement-Skipped") === "free-fallback"
802
+ };
803
+ }
804
+ return body;
628
805
  }
629
806
  /**
630
807
  * Make a request with automatic x402 payment handling.
@@ -654,7 +831,7 @@ var LLMClient = class _LLMClient {
654
831
  }
655
832
  throw new APIError(`API error: ${retryResp.status}`, retryResp.status, sanitizeErrorResponse(errorBody));
656
833
  }
657
- return retryResp.json();
834
+ return this.parseChatResponse(retryResp);
658
835
  }
659
836
  }
660
837
  if (response.status === 402) {
@@ -673,7 +850,7 @@ var LLMClient = class _LLMClient {
673
850
  sanitizeErrorResponse(errorBody)
674
851
  );
675
852
  }
676
- return response.json();
853
+ return this.parseChatResponse(response);
677
854
  }
678
855
  /**
679
856
  * Handle 402 response: parse requirements, sign payment, retry.
@@ -747,7 +924,8 @@ var LLMClient = class _LLMClient {
747
924
  const costUsd2 = parseFloat(details.amount) / 1e6;
748
925
  this.sessionCalls += 1;
749
926
  this.sessionTotalUsd += costUsd2;
750
- return retryResp2.json();
927
+ this.recordCost(url, costUsd2, { body, network: details.network });
928
+ return this.parseChatResponse(retryResp2);
751
929
  }
752
930
  }
753
931
  if (retryResponse.status === 402) {
@@ -769,7 +947,8 @@ var LLMClient = class _LLMClient {
769
947
  const costUsd = parseFloat(details.amount) / 1e6;
770
948
  this.sessionCalls += 1;
771
949
  this.sessionTotalUsd += costUsd;
772
- return retryResponse.json();
950
+ this.recordCost(url, costUsd, { body, network: details.network });
951
+ return this.parseChatResponse(retryResponse);
773
952
  }
774
953
  /**
775
954
  * Sign a payment header and return the PAYMENT-SIGNATURE value.
@@ -1018,6 +1197,7 @@ var LLMClient = class _LLMClient {
1018
1197
  const costUsd2 = parseFloat(details.amount) / 1e6;
1019
1198
  this.sessionCalls += 1;
1020
1199
  this.sessionTotalUsd += costUsd2;
1200
+ this.recordCost(url, costUsd2, { body, network: details.network });
1021
1201
  return retryResp2.json();
1022
1202
  }
1023
1203
  }
@@ -1040,6 +1220,7 @@ var LLMClient = class _LLMClient {
1040
1220
  const costUsd = parseFloat(details.amount) / 1e6;
1041
1221
  this.sessionCalls += 1;
1042
1222
  this.sessionTotalUsd += costUsd;
1223
+ this.recordCost(url, costUsd, { body, network: details.network });
1043
1224
  return retryResponse.json();
1044
1225
  }
1045
1226
  /**
@@ -1161,6 +1342,7 @@ var LLMClient = class _LLMClient {
1161
1342
  const costUsd2 = parseFloat(details.amount) / 1e6;
1162
1343
  this.sessionCalls += 1;
1163
1344
  this.sessionTotalUsd += costUsd2;
1345
+ this.recordCost(url, costUsd2, { network: details.network });
1164
1346
  return retryResp2.json();
1165
1347
  }
1166
1348
  }
@@ -1183,6 +1365,7 @@ var LLMClient = class _LLMClient {
1183
1365
  const costUsd = parseFloat(details.amount) / 1e6;
1184
1366
  this.sessionCalls += 1;
1185
1367
  this.sessionTotalUsd += costUsd;
1368
+ this.recordCost(url, costUsd, { network: details.network });
1186
1369
  return retryResponse.json();
1187
1370
  }
1188
1371
  /**
@@ -1202,9 +1385,59 @@ var LLMClient = class _LLMClient {
1202
1385
  }
1203
1386
  }
1204
1387
  /**
1205
- * List available LLM models with pricing.
1388
+ * List available models with pricing.
1389
+ *
1390
+ * Returns the full `/v1/models` unified catalog (chat + image + music).
1391
+ * The shape preserves backwards compatibility — image/music rows have
1392
+ * `inputPrice = outputPrice = 0` since those fields don't apply, and
1393
+ * their per-call price surfaces via `flatPrice`.
1206
1394
  */
1207
1395
  async listModels() {
1396
+ const raw = await this.fetchRawModels();
1397
+ return raw.map((m) => mapRawToModel(m));
1398
+ }
1399
+ /**
1400
+ * List available image generation models with pricing.
1401
+ *
1402
+ * The dedicated `/v1/images/models` endpoint was deprecated server-side;
1403
+ * image models live in the unified `/v1/models` catalog under
1404
+ * `categories: ["image", ...]`. This method filters that catalog so
1405
+ * existing callers keep working.
1406
+ */
1407
+ async listImageModels() {
1408
+ const raw = await this.fetchRawModels();
1409
+ return raw.filter((m) => Array.isArray(m.categories) && m.categories.includes("image")).map((m) => mapRawToImageModel(m));
1410
+ }
1411
+ /**
1412
+ * List all available models (chat, image, music, etc.) with pricing.
1413
+ *
1414
+ * @returns Array of all models with `type` field set from category
1415
+ * (`llm` for chat, `image` / `music` for media). Backwards-compat:
1416
+ * chat models always report `type: "llm"`.
1417
+ */
1418
+ async listAllModels() {
1419
+ const raw = await this.fetchRawModels();
1420
+ const out = [];
1421
+ for (const m of raw) {
1422
+ const cats = Array.isArray(m.categories) ? m.categories : [];
1423
+ if (cats.includes("image")) {
1424
+ const model = mapRawToImageModel(m);
1425
+ model.type = "image";
1426
+ out.push(model);
1427
+ } else {
1428
+ const model = mapRawToModel(m);
1429
+ model.type = "llm";
1430
+ out.push(model);
1431
+ }
1432
+ }
1433
+ return out;
1434
+ }
1435
+ /**
1436
+ * Internal: fetch the raw `/v1/models` catalog without normalising shape.
1437
+ * Used by listImageModels / listAllModels so each can pick category-
1438
+ * specific fields.
1439
+ */
1440
+ async fetchRawModels() {
1208
1441
  const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, {
1209
1442
  method: "GET"
1210
1443
  });
@@ -1222,65 +1455,8 @@ var LLMClient = class _LLMClient {
1222
1455
  );
1223
1456
  }
1224
1457
  const data = await response.json();
1225
- return (data.data || []).map((m) => ({
1226
- id: m.id,
1227
- name: m.name || m.id,
1228
- provider: m.provider || m.owned_by || "",
1229
- description: m.description || "",
1230
- inputPrice: m.inputPrice ?? m.input_price ?? m.pricing?.input ?? 0,
1231
- outputPrice: m.outputPrice ?? m.output_price ?? m.pricing?.output ?? 0,
1232
- contextWindow: m.contextWindow ?? m.context_window ?? 0,
1233
- maxOutput: m.maxOutput ?? m.max_output ?? 0,
1234
- categories: m.categories || [],
1235
- available: true,
1236
- billingMode: m.billingMode ?? m.billing_mode,
1237
- flatPrice: m.flatPrice ?? m.flat_price ?? m.pricing?.flat,
1238
- hidden: m.hidden
1239
- }));
1240
- }
1241
- /**
1242
- * List available image generation models with pricing.
1243
- */
1244
- async listImageModels() {
1245
- const response = await this.fetchWithTimeout(
1246
- `${this.apiUrl}/v1/images/models`,
1247
- { method: "GET" }
1248
- );
1249
- if (!response.ok) {
1250
- throw new APIError(
1251
- `Failed to list image models: ${response.status}`,
1252
- response.status
1253
- );
1254
- }
1255
- const data = await response.json();
1256
1458
  return data.data || [];
1257
1459
  }
1258
- /**
1259
- * List all available models (both LLM and image) with pricing.
1260
- *
1261
- * @returns Array of all models with 'type' field ('llm' or 'image')
1262
- *
1263
- * @example
1264
- * const models = await client.listAllModels();
1265
- * for (const model of models) {
1266
- * if (model.type === 'llm') {
1267
- * console.log(`LLM: ${model.id} - $${model.inputPrice}/M input`);
1268
- * } else {
1269
- * console.log(`Image: ${model.id} - $${model.pricePerImage}/image`);
1270
- * }
1271
- * }
1272
- */
1273
- async listAllModels() {
1274
- const llmModels = await this.listModels();
1275
- for (const model of llmModels) {
1276
- model.type = "llm";
1277
- }
1278
- const imageModels = await this.listImageModels();
1279
- for (const model of imageModels) {
1280
- model.type = "image";
1281
- }
1282
- return [...llmModels, ...imageModels];
1283
- }
1284
1460
  /**
1285
1461
  * Edit an image using img2img.
1286
1462
  *
@@ -1321,6 +1497,19 @@ var LLMClient = class _LLMClient {
1321
1497
  const data = await this.requestWithPaymentRaw("/v1/search", body);
1322
1498
  return data;
1323
1499
  }
1500
+ /**
1501
+ * Generic Exa endpoint proxy (POST). Useful when you need an Exa API
1502
+ * surface that the typed wrappers below don't expose.
1503
+ *
1504
+ * @param path - Exa endpoint segment: "search" | "find-similar" | "contents" | "answer"
1505
+ * @param body - Request body (see Exa API docs)
1506
+ *
1507
+ * @example
1508
+ * const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
1509
+ */
1510
+ async exa(path5, body) {
1511
+ return this.requestWithPaymentRaw(`/v1/exa/${path5}`, body);
1512
+ }
1324
1513
  /**
1325
1514
  * Neural web search via Exa. Returns semantically relevant URLs and metadata.
1326
1515
  * Understands meaning, not just keywords. $0.01/call.
@@ -1374,9 +1563,7 @@ var LLMClient = class _LLMClient {
1374
1563
  return data.data;
1375
1564
  }
1376
1565
  /**
1377
- * Get USDC balance on Base network.
1378
- *
1379
- * Automatically detects mainnet vs testnet based on API URL.
1566
+ * Get USDC balance on Base mainnet.
1380
1567
  *
1381
1568
  * @returns USDC balance as a float (6 decimal places normalized)
1382
1569
  *
@@ -1385,9 +1572,8 @@ var LLMClient = class _LLMClient {
1385
1572
  * console.log(`Balance: $${balance.toFixed(2)} USDC`);
1386
1573
  */
1387
1574
  async getBalance() {
1388
- const isTestnet = this.isTestnet();
1389
- const usdcContract = isTestnet ? "0x036CbD53842c5426634e7929541eC2318f3dCF7e" : "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1390
- 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"];
1575
+ const usdcContract = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1576
+ const rpcs = ["https://base.publicnode.com", "https://mainnet.base.org", "https://base.meowrpc.com"];
1391
1577
  const selector = "0x70a08231";
1392
1578
  const paddedAddress = this.account.address.slice(2).toLowerCase().padStart(64, "0");
1393
1579
  const data = selector + paddedAddress;
@@ -1718,19 +1904,7 @@ var LLMClient = class _LLMClient {
1718
1904
  getWalletAddress() {
1719
1905
  return this.account.address;
1720
1906
  }
1721
- /**
1722
- * Check if client is configured for testnet.
1723
- */
1724
- isTestnet() {
1725
- return this.apiUrl.includes("testnet.blockrun.ai");
1726
- }
1727
1907
  };
1728
- function testnetClient(options = {}) {
1729
- return new LLMClient({
1730
- ...options,
1731
- apiUrl: TESTNET_API_URL
1732
- });
1733
- }
1734
1908
  var client_default = LLMClient;
1735
1909
 
1736
1910
  // src/image.ts
@@ -1819,10 +1993,15 @@ var ImageClient = class {
1819
1993
  }
1820
1994
  /**
1821
1995
  * List available image generation models with pricing.
1996
+ *
1997
+ * The dedicated `/v1/images/models` endpoint was deprecated server-side;
1998
+ * image models live in the unified `/v1/models` catalog under
1999
+ * `categories: ["image", ...]`. This method filters that catalog so
2000
+ * existing callers keep working.
1822
2001
  */
1823
2002
  async listImageModels() {
1824
2003
  const response = await this.fetchWithTimeout(
1825
- `${this.apiUrl}/v1/images/models`,
2004
+ `${this.apiUrl}/v1/models`,
1826
2005
  { method: "GET" }
1827
2006
  );
1828
2007
  if (!response.ok) {
@@ -1832,7 +2011,16 @@ var ImageClient = class {
1832
2011
  );
1833
2012
  }
1834
2013
  const data = await response.json();
1835
- return data.data || [];
2014
+ return (data.data || []).filter((m) => Array.isArray(m.categories) && m.categories.includes("image")).map((m) => ({
2015
+ id: m.id,
2016
+ name: m.name || m.id,
2017
+ provider: m.provider || m.owned_by || "",
2018
+ description: m.description || "",
2019
+ pricePerImage: m.pricePerImage ?? m.price_per_image ?? m.pricing?.flat ?? m.flatPrice ?? m.flat_price ?? 0,
2020
+ supportedSizes: m.supportedSizes ?? m.supported_sizes,
2021
+ maxPromptLength: m.maxPromptLength ?? m.max_prompt_length,
2022
+ available: true
2023
+ }));
1836
2024
  }
1837
2025
  /**
1838
2026
  * Make a request with automatic x402 payment handling.
@@ -2727,13 +2915,13 @@ function buildUrl(base, query) {
2727
2915
 
2728
2916
  // src/wallet.ts
2729
2917
  var import_accounts8 = require("viem/accounts");
2730
- var fs = __toESM(require("fs"), 1);
2731
- var path = __toESM(require("path"), 1);
2732
- var os = __toESM(require("os"), 1);
2918
+ var fs2 = __toESM(require("fs"), 1);
2919
+ var path2 = __toESM(require("path"), 1);
2920
+ var os2 = __toESM(require("os"), 1);
2733
2921
  var USDC_BASE_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
2734
2922
  var BASE_CHAIN_ID2 = "8453";
2735
- var WALLET_DIR = path.join(os.homedir(), ".blockrun");
2736
- var WALLET_FILE = path.join(WALLET_DIR, ".session");
2923
+ var WALLET_DIR = path2.join(os2.homedir(), ".blockrun");
2924
+ var WALLET_FILE = path2.join(WALLET_DIR, ".session");
2737
2925
  function createWallet() {
2738
2926
  const privateKey = (0, import_accounts8.generatePrivateKey)();
2739
2927
  const account = (0, import_accounts8.privateKeyToAccount)(privateKey);
@@ -2743,27 +2931,27 @@ function createWallet() {
2743
2931
  };
2744
2932
  }
2745
2933
  function saveWallet(privateKey) {
2746
- if (!fs.existsSync(WALLET_DIR)) {
2747
- fs.mkdirSync(WALLET_DIR, { recursive: true });
2934
+ if (!fs2.existsSync(WALLET_DIR)) {
2935
+ fs2.mkdirSync(WALLET_DIR, { recursive: true });
2748
2936
  }
2749
- fs.writeFileSync(WALLET_FILE, privateKey, { mode: 384 });
2937
+ fs2.writeFileSync(WALLET_FILE, privateKey, { mode: 384 });
2750
2938
  return WALLET_FILE;
2751
2939
  }
2752
2940
  function scanWallets() {
2753
- const home = os.homedir();
2941
+ const home = os2.homedir();
2754
2942
  const results = [];
2755
2943
  try {
2756
- const entries = fs.readdirSync(home, { withFileTypes: true });
2944
+ const entries = fs2.readdirSync(home, { withFileTypes: true });
2757
2945
  for (const entry of entries) {
2758
2946
  if (!entry.name.startsWith(".") || !entry.isDirectory()) continue;
2759
- const walletFile = path.join(home, entry.name, "wallet.json");
2760
- if (!fs.existsSync(walletFile)) continue;
2947
+ const walletFile = path2.join(home, entry.name, "wallet.json");
2948
+ if (!fs2.existsSync(walletFile)) continue;
2761
2949
  try {
2762
- const data = JSON.parse(fs.readFileSync(walletFile, "utf-8"));
2950
+ const data = JSON.parse(fs2.readFileSync(walletFile, "utf-8"));
2763
2951
  const pk = data.privateKey || "";
2764
2952
  const addr = data.address || "";
2765
2953
  if (pk && addr) {
2766
- const mtime = fs.statSync(walletFile).mtimeMs;
2954
+ const mtime = fs2.statSync(walletFile).mtimeMs;
2767
2955
  results.push({ mtime, privateKey: pk, address: addr });
2768
2956
  }
2769
2957
  } catch {
@@ -2778,13 +2966,13 @@ function scanWallets() {
2778
2966
  function loadWallet() {
2779
2967
  const wallets = scanWallets();
2780
2968
  if (wallets.length > 0) return wallets[0].privateKey;
2781
- if (fs.existsSync(WALLET_FILE)) {
2782
- const key = fs.readFileSync(WALLET_FILE, "utf-8").trim();
2969
+ if (fs2.existsSync(WALLET_FILE)) {
2970
+ const key = fs2.readFileSync(WALLET_FILE, "utf-8").trim();
2783
2971
  if (key) return key;
2784
2972
  }
2785
- const legacyFile = path.join(WALLET_DIR, "wallet.key");
2786
- if (fs.existsSync(legacyFile)) {
2787
- const key = fs.readFileSync(legacyFile, "utf-8").trim();
2973
+ const legacyFile = path2.join(WALLET_DIR, "wallet.key");
2974
+ if (fs2.existsSync(legacyFile)) {
2975
+ const key = fs2.readFileSync(legacyFile, "utf-8").trim();
2788
2976
  if (key) return key;
2789
2977
  }
2790
2978
  return null;
@@ -2879,11 +3067,11 @@ var WALLET_FILE_PATH = WALLET_FILE;
2879
3067
  var WALLET_DIR_PATH = WALLET_DIR;
2880
3068
 
2881
3069
  // src/solana-wallet.ts
2882
- var fs2 = __toESM(require("fs"), 1);
2883
- var path2 = __toESM(require("path"), 1);
2884
- var os2 = __toESM(require("os"), 1);
2885
- var WALLET_DIR2 = path2.join(os2.homedir(), ".blockrun");
2886
- var SOLANA_WALLET_FILE = path2.join(WALLET_DIR2, ".solana-session");
3070
+ var fs3 = __toESM(require("fs"), 1);
3071
+ var path3 = __toESM(require("path"), 1);
3072
+ var os3 = __toESM(require("os"), 1);
3073
+ var WALLET_DIR2 = path3.join(os3.homedir(), ".blockrun");
3074
+ var SOLANA_WALLET_FILE = path3.join(WALLET_DIR2, ".solana-session");
2887
3075
  async function createSolanaWallet() {
2888
3076
  const { Keypair } = await import("@solana/web3.js");
2889
3077
  const bs58 = await import("bs58");
@@ -2912,39 +3100,39 @@ async function solanaPublicKey(privateKey) {
2912
3100
  return Keypair.fromSecretKey(bytes).publicKey.toBase58();
2913
3101
  }
2914
3102
  function saveSolanaWallet(privateKey) {
2915
- if (!fs2.existsSync(WALLET_DIR2)) fs2.mkdirSync(WALLET_DIR2, { recursive: true });
2916
- fs2.writeFileSync(SOLANA_WALLET_FILE, privateKey, { mode: 384 });
3103
+ if (!fs3.existsSync(WALLET_DIR2)) fs3.mkdirSync(WALLET_DIR2, { recursive: true });
3104
+ fs3.writeFileSync(SOLANA_WALLET_FILE, privateKey, { mode: 384 });
2917
3105
  return SOLANA_WALLET_FILE;
2918
3106
  }
2919
3107
  function scanSolanaWallets() {
2920
- const home = os2.homedir();
3108
+ const home = os3.homedir();
2921
3109
  const results = [];
2922
3110
  try {
2923
- const entries = fs2.readdirSync(home, { withFileTypes: true });
3111
+ const entries = fs3.readdirSync(home, { withFileTypes: true });
2924
3112
  for (const entry of entries) {
2925
3113
  if (!entry.name.startsWith(".") || !entry.isDirectory()) continue;
2926
- const solanaWalletFile = path2.join(home, entry.name, "solana-wallet.json");
2927
- if (fs2.existsSync(solanaWalletFile)) {
3114
+ const solanaWalletFile = path3.join(home, entry.name, "solana-wallet.json");
3115
+ if (fs3.existsSync(solanaWalletFile)) {
2928
3116
  try {
2929
- const data = JSON.parse(fs2.readFileSync(solanaWalletFile, "utf-8"));
3117
+ const data = JSON.parse(fs3.readFileSync(solanaWalletFile, "utf-8"));
2930
3118
  const pk = data.privateKey || "";
2931
3119
  const addr = data.address || "";
2932
3120
  if (pk && addr) {
2933
- const mtime = fs2.statSync(solanaWalletFile).mtimeMs;
3121
+ const mtime = fs3.statSync(solanaWalletFile).mtimeMs;
2934
3122
  results.push({ mtime, secretKey: pk, publicKey: addr });
2935
3123
  }
2936
3124
  } catch {
2937
3125
  }
2938
3126
  }
2939
3127
  if (entry.name === ".brcc") {
2940
- const brccWalletFile = path2.join(home, entry.name, "wallet.json");
2941
- if (fs2.existsSync(brccWalletFile)) {
3128
+ const brccWalletFile = path3.join(home, entry.name, "wallet.json");
3129
+ if (fs3.existsSync(brccWalletFile)) {
2942
3130
  try {
2943
- const data = JSON.parse(fs2.readFileSync(brccWalletFile, "utf-8"));
3131
+ const data = JSON.parse(fs3.readFileSync(brccWalletFile, "utf-8"));
2944
3132
  const pk = data.privateKey || "";
2945
3133
  const addr = data.address || "";
2946
3134
  if (pk && addr) {
2947
- const mtime = fs2.statSync(brccWalletFile).mtimeMs;
3135
+ const mtime = fs3.statSync(brccWalletFile).mtimeMs;
2948
3136
  results.push({ mtime, secretKey: pk, publicKey: addr });
2949
3137
  }
2950
3138
  } catch {
@@ -2960,8 +3148,8 @@ function scanSolanaWallets() {
2960
3148
  function loadSolanaWallet() {
2961
3149
  const wallets = scanSolanaWallets();
2962
3150
  if (wallets.length > 0) return wallets[0].secretKey;
2963
- if (fs2.existsSync(SOLANA_WALLET_FILE)) {
2964
- const key = fs2.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
3151
+ if (fs3.existsSync(SOLANA_WALLET_FILE)) {
3152
+ const key = fs3.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
2965
3153
  if (key) return key;
2966
3154
  }
2967
3155
  return null;
@@ -2976,8 +3164,8 @@ async function getOrCreateSolanaWallet() {
2976
3164
  if (wallets.length > 0) {
2977
3165
  return { privateKey: wallets[0].secretKey, address: wallets[0].publicKey, isNew: false };
2978
3166
  }
2979
- if (fs2.existsSync(SOLANA_WALLET_FILE)) {
2980
- const fileKey = fs2.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
3167
+ if (fs3.existsSync(SOLANA_WALLET_FILE)) {
3168
+ const fileKey = fs3.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
2981
3169
  if (fileKey) {
2982
3170
  const address2 = await solanaPublicKey(fileKey);
2983
3171
  return { privateKey: fileKey, address: address2, isNew: false };
@@ -3551,13 +3739,13 @@ function solanaClient(options = {}) {
3551
3739
  }
3552
3740
 
3553
3741
  // src/cache.ts
3554
- var fs3 = __toESM(require("fs"), 1);
3555
- var path3 = __toESM(require("path"), 1);
3556
- var os3 = __toESM(require("os"), 1);
3742
+ var fs4 = __toESM(require("fs"), 1);
3743
+ var path4 = __toESM(require("path"), 1);
3744
+ var os4 = __toESM(require("os"), 1);
3557
3745
  var crypto2 = __toESM(require("crypto"), 1);
3558
- var CACHE_DIR = path3.join(os3.homedir(), ".blockrun", "cache");
3559
- var DATA_DIR = path3.join(os3.homedir(), ".blockrun", "data");
3560
- var COST_LOG_FILE = path3.join(os3.homedir(), ".blockrun", "cost_log.jsonl");
3746
+ var CACHE_DIR = path4.join(os4.homedir(), ".blockrun", "cache");
3747
+ var DATA_DIR = path4.join(os4.homedir(), ".blockrun", "data");
3748
+ var COST_LOG_FILE2 = path4.join(os4.homedir(), ".blockrun", "cost_log.jsonl");
3561
3749
  var DEFAULT_TTL = {
3562
3750
  "/v1/x/": 3600 * 1e3,
3563
3751
  "/v1/partner/": 3600 * 1e3,
@@ -3578,19 +3766,19 @@ function cacheKey(endpoint, body) {
3578
3766
  return crypto2.createHash("sha256").update(keyData).digest("hex").slice(0, 16);
3579
3767
  }
3580
3768
  function cachePath(key) {
3581
- return path3.join(CACHE_DIR, `${key}.json`);
3769
+ return path4.join(CACHE_DIR, `${key}.json`);
3582
3770
  }
3583
3771
  function getCached(key) {
3584
3772
  const filePath = cachePath(key);
3585
- if (!fs3.existsSync(filePath)) return null;
3773
+ if (!fs4.existsSync(filePath)) return null;
3586
3774
  try {
3587
- const raw = fs3.readFileSync(filePath, "utf-8");
3775
+ const raw = fs4.readFileSync(filePath, "utf-8");
3588
3776
  const entry = JSON.parse(raw);
3589
3777
  const ttl = entry.ttlMs ?? getTtl(entry.endpoint ?? "");
3590
3778
  if (ttl <= 0) return null;
3591
3779
  if (Date.now() - entry.cachedAt > ttl) {
3592
3780
  try {
3593
- fs3.unlinkSync(filePath);
3781
+ fs4.unlinkSync(filePath);
3594
3782
  } catch {
3595
3783
  }
3596
3784
  return null;
@@ -3609,7 +3797,7 @@ function getCachedByRequest(endpoint, body) {
3609
3797
  function setCache(key, data, ttlMs) {
3610
3798
  if (ttlMs <= 0) return;
3611
3799
  try {
3612
- fs3.mkdirSync(CACHE_DIR, { recursive: true });
3800
+ fs4.mkdirSync(CACHE_DIR, { recursive: true });
3613
3801
  } catch {
3614
3802
  }
3615
3803
  const entry = {
@@ -3618,7 +3806,7 @@ function setCache(key, data, ttlMs) {
3618
3806
  ttlMs
3619
3807
  };
3620
3808
  try {
3621
- fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
3809
+ fs4.writeFileSync(cachePath(key), JSON.stringify(entry));
3622
3810
  } catch {
3623
3811
  }
3624
3812
  }
@@ -3641,7 +3829,7 @@ function readableFilename(endpoint, body) {
3641
3829
  }
3642
3830
  function saveReadable(endpoint, body, response, costUsd) {
3643
3831
  try {
3644
- fs3.mkdirSync(DATA_DIR, { recursive: true });
3832
+ fs4.mkdirSync(DATA_DIR, { recursive: true });
3645
3833
  } catch {
3646
3834
  }
3647
3835
  const filename = readableFilename(endpoint, body);
@@ -3653,14 +3841,14 @@ function saveReadable(endpoint, body, response, costUsd) {
3653
3841
  response
3654
3842
  };
3655
3843
  try {
3656
- fs3.writeFileSync(path3.join(DATA_DIR, filename), JSON.stringify(entry, null, 2));
3844
+ fs4.writeFileSync(path4.join(DATA_DIR, filename), JSON.stringify(entry, null, 2));
3657
3845
  } catch {
3658
3846
  }
3659
3847
  }
3660
3848
  function appendCostLog(endpoint, costUsd) {
3661
3849
  if (costUsd <= 0) return;
3662
3850
  try {
3663
- fs3.mkdirSync(path3.dirname(COST_LOG_FILE), { recursive: true });
3851
+ fs4.mkdirSync(path4.dirname(COST_LOG_FILE2), { recursive: true });
3664
3852
  } catch {
3665
3853
  }
3666
3854
  const entry = {
@@ -3669,7 +3857,7 @@ function appendCostLog(endpoint, costUsd) {
3669
3857
  cost_usd: costUsd
3670
3858
  };
3671
3859
  try {
3672
- fs3.appendFileSync(COST_LOG_FILE, JSON.stringify(entry) + "\n");
3860
+ fs4.appendFileSync(COST_LOG_FILE2, JSON.stringify(entry) + "\n");
3673
3861
  } catch {
3674
3862
  }
3675
3863
  }
@@ -3677,7 +3865,7 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
3677
3865
  const ttl = getTtl(endpoint);
3678
3866
  if (ttl > 0) {
3679
3867
  try {
3680
- fs3.mkdirSync(CACHE_DIR, { recursive: true });
3868
+ fs4.mkdirSync(CACHE_DIR, { recursive: true });
3681
3869
  } catch {
3682
3870
  }
3683
3871
  const key = cacheKey(endpoint, body);
@@ -3689,7 +3877,7 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
3689
3877
  costUsd
3690
3878
  };
3691
3879
  try {
3692
- fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
3880
+ fs4.writeFileSync(cachePath(key), JSON.stringify(entry));
3693
3881
  } catch {
3694
3882
  }
3695
3883
  }
@@ -3697,14 +3885,14 @@ function saveToCache(endpoint, body, response, costUsd = 0) {
3697
3885
  appendCostLog(endpoint, costUsd);
3698
3886
  }
3699
3887
  function clearCache() {
3700
- if (!fs3.existsSync(CACHE_DIR)) return 0;
3888
+ if (!fs4.existsSync(CACHE_DIR)) return 0;
3701
3889
  let count = 0;
3702
3890
  try {
3703
- const files = fs3.readdirSync(CACHE_DIR);
3891
+ const files = fs4.readdirSync(CACHE_DIR);
3704
3892
  for (const file of files) {
3705
3893
  if (file.endsWith(".json")) {
3706
3894
  try {
3707
- fs3.unlinkSync(path3.join(CACHE_DIR, file));
3895
+ fs4.unlinkSync(path4.join(CACHE_DIR, file));
3708
3896
  count++;
3709
3897
  } catch {
3710
3898
  }
@@ -3715,14 +3903,14 @@ function clearCache() {
3715
3903
  return count;
3716
3904
  }
3717
3905
  function getCostLogSummary() {
3718
- if (!fs3.existsSync(COST_LOG_FILE)) {
3906
+ if (!fs4.existsSync(COST_LOG_FILE2)) {
3719
3907
  return { totalUsd: 0, calls: 0, byEndpoint: {} };
3720
3908
  }
3721
3909
  let totalUsd = 0;
3722
3910
  let calls = 0;
3723
3911
  const byEndpoint = {};
3724
3912
  try {
3725
- const content = fs3.readFileSync(COST_LOG_FILE, "utf-8").trim();
3913
+ const content = fs4.readFileSync(COST_LOG_FILE2, "utf-8").trim();
3726
3914
  if (!content) return { totalUsd: 0, calls: 0, byEndpoint: {} };
3727
3915
  for (const line of content.split("\n")) {
3728
3916
  if (!line) continue;
@@ -3777,47 +3965,6 @@ async function status() {
3777
3965
  return { address, balance };
3778
3966
  }
3779
3967
 
3780
- // src/cost-log.ts
3781
- var fs4 = __toESM(require("fs"), 1);
3782
- var path4 = __toESM(require("path"), 1);
3783
- var os4 = __toESM(require("os"), 1);
3784
- var DATA_DIR2 = path4.join(os4.homedir(), ".blockrun", "data");
3785
- var COST_LOG_FILE2 = path4.join(DATA_DIR2, "costs.jsonl");
3786
- function logCost(entry) {
3787
- try {
3788
- fs4.mkdirSync(DATA_DIR2, { recursive: true });
3789
- } catch {
3790
- }
3791
- try {
3792
- fs4.appendFileSync(COST_LOG_FILE2, JSON.stringify(entry) + "\n");
3793
- } catch {
3794
- }
3795
- }
3796
- function getCostSummary() {
3797
- if (!fs4.existsSync(COST_LOG_FILE2)) {
3798
- return { totalUsd: 0, calls: 0, byModel: {} };
3799
- }
3800
- let totalUsd = 0;
3801
- let calls = 0;
3802
- const byModel = {};
3803
- try {
3804
- const content = fs4.readFileSync(COST_LOG_FILE2, "utf-8").trim();
3805
- if (!content) return { totalUsd: 0, calls: 0, byModel: {} };
3806
- for (const line of content.split("\n")) {
3807
- if (!line) continue;
3808
- try {
3809
- const entry = JSON.parse(line);
3810
- totalUsd += entry.costUsd;
3811
- calls += 1;
3812
- byModel[entry.model] = (byModel[entry.model] || 0) + entry.costUsd;
3813
- } catch {
3814
- }
3815
- }
3816
- } catch {
3817
- }
3818
- return { totalUsd, calls, byModel };
3819
- }
3820
-
3821
3968
  // src/openai-compat.ts
3822
3969
  var StreamingResponse = class {
3823
3970
  reader;
@@ -4125,7 +4272,6 @@ var AnthropicClient = class {
4125
4272
  solanaKeyToBytes,
4126
4273
  solanaPublicKey,
4127
4274
  status,
4128
- testnetClient,
4129
4275
  validateMaxTokens,
4130
4276
  validateModel,
4131
4277
  validateTemperature,