@blockrun/llm 1.4.3 → 1.6.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.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  BASE_CHAIN_ID: () => BASE_CHAIN_ID,
36
36
  BlockrunError: () => BlockrunError,
37
37
  ImageClient: () => ImageClient,
38
+ KNOWN_PROVIDERS: () => KNOWN_PROVIDERS,
38
39
  LLMClient: () => LLMClient,
39
40
  OpenAI: () => OpenAI,
40
41
  PaymentError: () => PaymentError,
@@ -58,6 +59,7 @@ __export(index_exports, {
58
59
  formatWalletCreatedMessage: () => formatWalletCreatedMessage,
59
60
  getCached: () => getCached,
60
61
  getCachedByRequest: () => getCachedByRequest,
62
+ getCostLogSummary: () => getCostLogSummary,
61
63
  getCostSummary: () => getCostSummary,
62
64
  getEip681Uri: () => getEip681Uri,
63
65
  getOrCreateSolanaWallet: () => getOrCreateSolanaWallet,
@@ -80,7 +82,11 @@ __export(index_exports, {
80
82
  solanaKeyToBytes: () => solanaKeyToBytes,
81
83
  solanaPublicKey: () => solanaPublicKey,
82
84
  status: () => status,
83
- testnetClient: () => testnetClient
85
+ testnetClient: () => testnetClient,
86
+ validateMaxTokens: () => validateMaxTokens,
87
+ validateModel: () => validateModel,
88
+ validateTemperature: () => validateTemperature,
89
+ validateTopP: () => validateTopP
84
90
  });
85
91
  module.exports = __toCommonJS(index_exports);
86
92
 
@@ -302,6 +308,61 @@ function extractPaymentDetails(paymentRequired, preferredNetwork) {
302
308
 
303
309
  // src/validation.ts
304
310
  var LOCALHOST_DOMAINS = ["localhost", "127.0.0.1"];
311
+ var KNOWN_PROVIDERS = /* @__PURE__ */ new Set([
312
+ "openai",
313
+ "anthropic",
314
+ "google",
315
+ "deepseek",
316
+ "mistralai",
317
+ "meta-llama",
318
+ "together",
319
+ "xai",
320
+ "moonshot",
321
+ "nvidia",
322
+ "minimax",
323
+ "zai"
324
+ ]);
325
+ function validateModel(model) {
326
+ if (!model || typeof model !== "string") {
327
+ throw new Error("Model must be a non-empty string");
328
+ }
329
+ }
330
+ function validateMaxTokens(maxTokens) {
331
+ if (maxTokens === void 0 || maxTokens === null) {
332
+ return;
333
+ }
334
+ if (typeof maxTokens !== "number" || !Number.isInteger(maxTokens)) {
335
+ throw new Error("maxTokens must be an integer");
336
+ }
337
+ if (maxTokens < 1) {
338
+ throw new Error("maxTokens must be positive (minimum: 1)");
339
+ }
340
+ if (maxTokens > 1e5) {
341
+ throw new Error("maxTokens too large (maximum: 100000)");
342
+ }
343
+ }
344
+ function validateTemperature(temperature) {
345
+ if (temperature === void 0 || temperature === null) {
346
+ return;
347
+ }
348
+ if (typeof temperature !== "number") {
349
+ throw new Error("temperature must be a number");
350
+ }
351
+ if (temperature < 0 || temperature > 2) {
352
+ throw new Error("temperature must be between 0 and 2");
353
+ }
354
+ }
355
+ function validateTopP(topP) {
356
+ if (topP === void 0 || topP === null) {
357
+ return;
358
+ }
359
+ if (typeof topP !== "number") {
360
+ throw new Error("topP must be a number");
361
+ }
362
+ if (topP < 0 || topP > 1) {
363
+ throw new Error("topP must be between 0 and 1");
364
+ }
365
+ }
305
366
  function validatePrivateKey(key) {
306
367
  if (typeof key !== "string") {
307
368
  throw new Error("Private key must be a string");
@@ -377,9 +438,9 @@ var DEFAULT_API_URL = "https://blockrun.ai/api";
377
438
  var TESTNET_API_URL = "https://testnet.blockrun.ai/api";
378
439
  var DEFAULT_MAX_TOKENS = 1024;
379
440
  var DEFAULT_TIMEOUT = 6e4;
380
- var SDK_VERSION = "0.3.0";
441
+ var SDK_VERSION = "1.5.0";
381
442
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
382
- var LLMClient = class {
443
+ var LLMClient = class _LLMClient {
383
444
  static DEFAULT_API_URL = DEFAULT_API_URL;
384
445
  static TESTNET_API_URL = TESTNET_API_URL;
385
446
  account;
@@ -390,6 +451,11 @@ var LLMClient = class {
390
451
  sessionCalls = 0;
391
452
  modelPricingCache = null;
392
453
  modelPricingPromise = null;
454
+ // Pre-auth cache: avoids the 402 round-trip on repeat requests to the same model.
455
+ // Key = "endpoint:model", value = cached payment header + timestamp.
456
+ // TTL: 1 hour (mirrors ClawRouter's payment-preauth.ts approach).
457
+ preAuthCache = /* @__PURE__ */ new Map();
458
+ static PRE_AUTH_TTL_MS = 36e5;
393
459
  /**
394
460
  * Initialize the BlockRun LLM client.
395
461
  *
@@ -414,13 +480,13 @@ var LLMClient = class {
414
480
  /**
415
481
  * Simple 1-line chat interface.
416
482
  *
417
- * @param model - Model ID (e.g., 'openai/gpt-4o', 'anthropic/claude-sonnet-4')
483
+ * @param model - Model ID (e.g., 'openai/gpt-5.2', 'anthropic/claude-sonnet-4.6')
418
484
  * @param prompt - User message
419
485
  * @param options - Optional chat parameters
420
486
  * @returns Assistant's response text
421
487
  *
422
488
  * @example
423
- * const response = await client.chat('gpt-4o', 'What is the capital of France?');
489
+ * const response = await client.chat('gpt-5.2', 'What is the capital of France?');
424
490
  * console.log(response); // 'The capital of France is Paris.'
425
491
  */
426
492
  async chat(model, prompt, options) {
@@ -566,6 +632,27 @@ var LLMClient = class {
566
632
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
567
633
  body: JSON.stringify(body)
568
634
  });
635
+ if (response.status === 502 || response.status === 503) {
636
+ await new Promise((r) => setTimeout(r, 1e3));
637
+ const retryResp = await this.fetchWithTimeout(url, {
638
+ method: "POST",
639
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
640
+ body: JSON.stringify(body)
641
+ });
642
+ if (retryResp.status !== 502 && retryResp.status !== 503) {
643
+ if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp);
644
+ if (!retryResp.ok) {
645
+ let errorBody;
646
+ try {
647
+ errorBody = await retryResp.json();
648
+ } catch {
649
+ errorBody = { error: "Request failed" };
650
+ }
651
+ throw new APIError(`API error: ${retryResp.status}`, retryResp.status, sanitizeErrorResponse(errorBody));
652
+ }
653
+ return retryResp.json();
654
+ }
655
+ }
569
656
  if (response.status === 402) {
570
657
  return this.handlePaymentAndRetry(url, body, response);
571
658
  }
@@ -631,6 +718,34 @@ var LLMClient = class {
631
718
  },
632
719
  body: JSON.stringify(body)
633
720
  });
721
+ if (retryResponse.status === 502 || retryResponse.status === 503) {
722
+ await new Promise((r) => setTimeout(r, 1e3));
723
+ const retryResp2 = await this.fetchWithTimeout(url, {
724
+ method: "POST",
725
+ headers: {
726
+ "Content-Type": "application/json",
727
+ "User-Agent": USER_AGENT,
728
+ "PAYMENT-SIGNATURE": paymentPayload
729
+ },
730
+ body: JSON.stringify(body)
731
+ });
732
+ if (retryResp2.status !== 502 && retryResp2.status !== 503) {
733
+ if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
734
+ if (!retryResp2.ok) {
735
+ let errorBody;
736
+ try {
737
+ errorBody = await retryResp2.json();
738
+ } catch {
739
+ errorBody = { error: "Request failed" };
740
+ }
741
+ throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
742
+ }
743
+ const costUsd2 = parseFloat(details.amount) / 1e6;
744
+ this.sessionCalls += 1;
745
+ this.sessionTotalUsd += costUsd2;
746
+ return retryResp2.json();
747
+ }
748
+ }
634
749
  if (retryResponse.status === 402) {
635
750
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
636
751
  }
@@ -652,6 +767,131 @@ var LLMClient = class {
652
767
  this.sessionTotalUsd += costUsd;
653
768
  return retryResponse.json();
654
769
  }
770
+ /**
771
+ * Sign a payment header and return the PAYMENT-SIGNATURE value.
772
+ * Extracted to share logic between streaming and non-streaming flows.
773
+ */
774
+ async signPayment(paymentHeader) {
775
+ const paymentRequired = parsePaymentRequired(paymentHeader);
776
+ const details = extractPaymentDetails(paymentRequired);
777
+ const extensions = paymentRequired.extensions;
778
+ const paymentPayload = await createPaymentPayload(
779
+ this.privateKey,
780
+ this.account.address,
781
+ details.recipient,
782
+ details.amount,
783
+ details.network || "eip155:8453",
784
+ {
785
+ resourceUrl: validateResourceUrl(
786
+ details.resource?.url || `${this.apiUrl}/v1/chat/completions`,
787
+ this.apiUrl
788
+ ),
789
+ resourceDescription: details.resource?.description || "BlockRun AI API call",
790
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
791
+ extra: details.extra,
792
+ extensions
793
+ }
794
+ );
795
+ const costUsd = parseFloat(details.amount) / 1e6;
796
+ return { paymentPayload, costUsd };
797
+ }
798
+ /**
799
+ * Streaming chat completion with automatic x402 payment.
800
+ *
801
+ * Uses a pre-auth cache so repeat calls to the same model skip the 402
802
+ * round-trip (~200ms savings). Falls back to the normal 402 flow on cache
803
+ * miss or if the pre-signed payment is rejected.
804
+ *
805
+ * @returns Raw fetch Response with a streaming SSE body.
806
+ */
807
+ async chatCompletionStream(model, messages, options) {
808
+ const url = `${this.apiUrl}/v1/chat/completions`;
809
+ const body = {
810
+ model,
811
+ messages,
812
+ max_tokens: options?.maxTokens ?? DEFAULT_MAX_TOKENS,
813
+ stream: true
814
+ };
815
+ if (options?.temperature !== void 0) body.temperature = options.temperature;
816
+ if (options?.topP !== void 0) body.top_p = options.topP;
817
+ if (options?.tools !== void 0) body.tools = options.tools;
818
+ if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
819
+ const cacheKey2 = `/v1/chat/completions:${model}`;
820
+ const cached = this.preAuthCache.get(cacheKey2);
821
+ const now = Date.now();
822
+ if (cached && now - cached.cachedAt < _LLMClient.PRE_AUTH_TTL_MS) {
823
+ try {
824
+ const { paymentPayload: paymentPayload2, costUsd: costUsd2 } = await this.signPayment(cached.paymentHeader);
825
+ const preAuthResp = await this.fetchWithTimeout(url, {
826
+ method: "POST",
827
+ headers: {
828
+ "Content-Type": "application/json",
829
+ "User-Agent": USER_AGENT,
830
+ "PAYMENT-SIGNATURE": paymentPayload2
831
+ },
832
+ body: JSON.stringify(body)
833
+ });
834
+ if (preAuthResp.status !== 402 && preAuthResp.ok) {
835
+ this.sessionCalls += 1;
836
+ this.sessionTotalUsd += costUsd2;
837
+ return preAuthResp;
838
+ }
839
+ this.preAuthCache.delete(cacheKey2);
840
+ } catch {
841
+ this.preAuthCache.delete(cacheKey2);
842
+ }
843
+ }
844
+ const firstResp = await this.fetchWithTimeout(url, {
845
+ method: "POST",
846
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
847
+ body: JSON.stringify(body)
848
+ });
849
+ if (firstResp.status !== 402) {
850
+ if (!firstResp.ok) {
851
+ let errorBody;
852
+ try {
853
+ errorBody = await firstResp.json();
854
+ } catch {
855
+ errorBody = { error: "Request failed" };
856
+ }
857
+ throw new APIError(`API error: ${firstResp.status}`, firstResp.status, sanitizeErrorResponse(errorBody));
858
+ }
859
+ return firstResp;
860
+ }
861
+ let paymentHeader = firstResp.headers.get("payment-required");
862
+ if (!paymentHeader) {
863
+ try {
864
+ const rb = await firstResp.json();
865
+ if (rb.x402 || rb.accepts) paymentHeader = btoa(JSON.stringify(rb));
866
+ } catch {
867
+ }
868
+ }
869
+ if (!paymentHeader) throw new PaymentError("402 response but no payment requirements found");
870
+ this.preAuthCache.set(cacheKey2, { paymentHeader, cachedAt: now });
871
+ const { paymentPayload, costUsd } = await this.signPayment(paymentHeader);
872
+ const streamResp = await this.fetchWithTimeout(url, {
873
+ method: "POST",
874
+ headers: {
875
+ "Content-Type": "application/json",
876
+ "User-Agent": USER_AGENT,
877
+ "PAYMENT-SIGNATURE": paymentPayload
878
+ },
879
+ body: JSON.stringify(body)
880
+ });
881
+ if (streamResp.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
882
+ if (!streamResp.ok) {
883
+ let errorBody;
884
+ try {
885
+ errorBody = await streamResp.json();
886
+ } catch {
887
+ errorBody = { error: "Request failed" };
888
+ }
889
+ throw new APIError(`API error after payment: ${streamResp.status}`, streamResp.status, sanitizeErrorResponse(errorBody));
890
+ }
891
+ this.sessionCalls += 1;
892
+ this.sessionTotalUsd += costUsd;
893
+ return streamResp;
894
+ }
655
895
  /**
656
896
  * Make a request with automatic x402 payment handling, returning raw JSON.
657
897
  * Used for non-ChatResponse endpoints (X/Twitter, search, image edit, etc.).
@@ -663,6 +903,27 @@ var LLMClient = class {
663
903
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
664
904
  body: JSON.stringify(body)
665
905
  });
906
+ if (response.status === 502 || response.status === 503) {
907
+ await new Promise((r) => setTimeout(r, 1e3));
908
+ const retryResp = await this.fetchWithTimeout(url, {
909
+ method: "POST",
910
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
911
+ body: JSON.stringify(body)
912
+ });
913
+ if (retryResp.status !== 502 && retryResp.status !== 503) {
914
+ if (retryResp.status === 402) return this.handlePaymentAndRetryRaw(url, body, retryResp);
915
+ if (!retryResp.ok) {
916
+ let errorBody;
917
+ try {
918
+ errorBody = await retryResp.json();
919
+ } catch {
920
+ errorBody = { error: "Request failed" };
921
+ }
922
+ throw new APIError(`API error: ${retryResp.status}`, retryResp.status, sanitizeErrorResponse(errorBody));
923
+ }
924
+ return retryResp.json();
925
+ }
926
+ }
666
927
  if (response.status === 402) {
667
928
  return this.handlePaymentAndRetryRaw(url, body, response);
668
929
  }
@@ -728,6 +989,34 @@ var LLMClient = class {
728
989
  },
729
990
  body: JSON.stringify(body)
730
991
  });
992
+ if (retryResponse.status === 502 || retryResponse.status === 503) {
993
+ await new Promise((r) => setTimeout(r, 1e3));
994
+ const retryResp2 = await this.fetchWithTimeout(url, {
995
+ method: "POST",
996
+ headers: {
997
+ "Content-Type": "application/json",
998
+ "User-Agent": USER_AGENT,
999
+ "PAYMENT-SIGNATURE": paymentPayload
1000
+ },
1001
+ body: JSON.stringify(body)
1002
+ });
1003
+ if (retryResp2.status !== 502 && retryResp2.status !== 503) {
1004
+ if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
1005
+ if (!retryResp2.ok) {
1006
+ let errorBody;
1007
+ try {
1008
+ errorBody = await retryResp2.json();
1009
+ } catch {
1010
+ errorBody = { error: "Request failed" };
1011
+ }
1012
+ throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
1013
+ }
1014
+ const costUsd2 = parseFloat(details.amount) / 1e6;
1015
+ this.sessionCalls += 1;
1016
+ this.sessionTotalUsd += costUsd2;
1017
+ return retryResp2.json();
1018
+ }
1019
+ }
731
1020
  if (retryResponse.status === 402) {
732
1021
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
733
1022
  }
@@ -760,6 +1049,26 @@ var LLMClient = class {
760
1049
  method: "GET",
761
1050
  headers: { "User-Agent": USER_AGENT }
762
1051
  });
1052
+ if (response.status === 502 || response.status === 503) {
1053
+ await new Promise((r) => setTimeout(r, 1e3));
1054
+ const retryResp = await this.fetchWithTimeout(url, {
1055
+ method: "GET",
1056
+ headers: { "User-Agent": USER_AGENT }
1057
+ });
1058
+ if (retryResp.status !== 502 && retryResp.status !== 503) {
1059
+ if (retryResp.status === 402) return this.handleGetPaymentAndRetryRaw(url, endpoint, params, retryResp);
1060
+ if (!retryResp.ok) {
1061
+ let errorBody;
1062
+ try {
1063
+ errorBody = await retryResp.json();
1064
+ } catch {
1065
+ errorBody = { error: "Request failed" };
1066
+ }
1067
+ throw new APIError(`API error: ${retryResp.status}`, retryResp.status, sanitizeErrorResponse(errorBody));
1068
+ }
1069
+ return retryResp.json();
1070
+ }
1071
+ }
763
1072
  if (response.status === 402) {
764
1073
  return this.handleGetPaymentAndRetryRaw(url, endpoint, params, response);
765
1074
  }
@@ -825,6 +1134,32 @@ var LLMClient = class {
825
1134
  "PAYMENT-SIGNATURE": paymentPayload
826
1135
  }
827
1136
  });
1137
+ if (retryResponse.status === 502 || retryResponse.status === 503) {
1138
+ await new Promise((r) => setTimeout(r, 1e3));
1139
+ const retryResp2 = await this.fetchWithTimeout(retryUrl, {
1140
+ method: "GET",
1141
+ headers: {
1142
+ "User-Agent": USER_AGENT,
1143
+ "PAYMENT-SIGNATURE": paymentPayload
1144
+ }
1145
+ });
1146
+ if (retryResp2.status !== 502 && retryResp2.status !== 503) {
1147
+ if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
1148
+ if (!retryResp2.ok) {
1149
+ let errorBody;
1150
+ try {
1151
+ errorBody = await retryResp2.json();
1152
+ } catch {
1153
+ errorBody = { error: "Request failed" };
1154
+ }
1155
+ throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
1156
+ }
1157
+ const costUsd2 = parseFloat(details.amount) / 1e6;
1158
+ this.sessionCalls += 1;
1159
+ this.sessionTotalUsd += costUsd2;
1160
+ return retryResp2.json();
1161
+ }
1162
+ }
828
1163
  if (retryResponse.status === 402) {
829
1164
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
830
1165
  }
@@ -886,12 +1221,12 @@ var LLMClient = class {
886
1221
  return (data.data || []).map((m) => ({
887
1222
  id: m.id,
888
1223
  name: m.name || m.id,
889
- provider: m.owned_by || "",
1224
+ provider: m.provider || m.owned_by || "",
890
1225
  description: m.description || "",
891
- inputPrice: m.pricing?.input ?? m.pricing?.flat ?? 0,
892
- outputPrice: m.pricing?.output ?? 0,
893
- contextWindow: m.context_window || 0,
894
- maxOutput: m.max_output || 0,
1226
+ inputPrice: m.inputPrice ?? m.input_price ?? m.pricing?.input ?? 0,
1227
+ outputPrice: m.outputPrice ?? m.output_price ?? m.pricing?.output ?? 0,
1228
+ contextWindow: m.contextWindow ?? m.context_window ?? 0,
1229
+ maxOutput: m.maxOutput ?? m.max_output ?? 0,
895
1230
  categories: m.categories || [],
896
1231
  available: true
897
1232
  }));
@@ -979,6 +1314,58 @@ var LLMClient = class {
979
1314
  const data = await this.requestWithPaymentRaw("/v1/search", body);
980
1315
  return data;
981
1316
  }
1317
+ /**
1318
+ * Neural web search via Exa. Returns semantically relevant URLs and metadata.
1319
+ * Understands meaning, not just keywords. $0.01/call.
1320
+ *
1321
+ * @param query - Natural language search query
1322
+ * @param options - Optional filters (numResults, category, date range, domains)
1323
+ */
1324
+ async exaSearch(query, options) {
1325
+ const body = { query };
1326
+ if (options?.numResults !== void 0) body.numResults = options.numResults;
1327
+ if (options?.category !== void 0) body.category = options.category;
1328
+ if (options?.startPublishedDate !== void 0) body.startPublishedDate = options.startPublishedDate;
1329
+ if (options?.endPublishedDate !== void 0) body.endPublishedDate = options.endPublishedDate;
1330
+ if (options?.includeDomains !== void 0) body.includeDomains = options.includeDomains;
1331
+ if (options?.excludeDomains !== void 0) body.excludeDomains = options.excludeDomains;
1332
+ const data = await this.requestWithPaymentRaw("/v1/exa/search", body);
1333
+ return data.data;
1334
+ }
1335
+ /**
1336
+ * Ask a question and get a cited, synthesized answer grounded in real web sources.
1337
+ * No hallucinations — every claim is backed by a citation. $0.01/call.
1338
+ *
1339
+ * @param query - The question to answer
1340
+ */
1341
+ async exaAnswer(query) {
1342
+ const data = await this.requestWithPaymentRaw("/v1/exa/answer", { query });
1343
+ return data.data;
1344
+ }
1345
+ /**
1346
+ * Fetch full Markdown text content from a list of URLs. $0.002 per URL.
1347
+ * Returns clean text ready to feed into an LLM context window.
1348
+ *
1349
+ * @param urls - Array of URLs to fetch (up to 100)
1350
+ */
1351
+ async exaContents(urls) {
1352
+ const data = await this.requestWithPaymentRaw("/v1/exa/contents", { urls });
1353
+ return data.data;
1354
+ }
1355
+ /**
1356
+ * Find pages semantically similar to a given URL. $0.01/call.
1357
+ * Useful for discovering competitors, alternatives, and related resources.
1358
+ *
1359
+ * @param url - Reference URL
1360
+ * @param options - Optional filters (numResults, excludeSourceDomain)
1361
+ */
1362
+ async exaFindSimilar(url, options) {
1363
+ const body = { url };
1364
+ if (options?.numResults !== void 0) body.numResults = options.numResults;
1365
+ if (options?.excludeSourceDomain !== void 0) body.excludeSourceDomain = options.excludeSourceDomain;
1366
+ const data = await this.requestWithPaymentRaw("/v1/exa/find-similar", body);
1367
+ return data.data;
1368
+ }
982
1369
  /**
983
1370
  * Get USDC balance on Base network.
984
1371
  *
@@ -1992,6 +2379,55 @@ var SolanaLLMClient = class {
1992
2379
  async pmQuery(path5, query) {
1993
2380
  return this.requestWithPaymentRaw(`/v1/pm/${path5}`, query);
1994
2381
  }
2382
+ // ── Exa Web Search (Powered by Exa) ──────────────────────────────────────
2383
+ /**
2384
+ * Generic Exa endpoint proxy (POST, Solana payment). Powered by Exa.
2385
+ *
2386
+ * @param path - Exa endpoint: "search" | "find-similar" | "contents" | "answer"
2387
+ * @param body - Request body (see Exa API docs)
2388
+ *
2389
+ * @example
2390
+ * const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
2391
+ */
2392
+ async exa(path5, body) {
2393
+ return this.requestWithPaymentRaw(`/v1/exa/${path5}`, body);
2394
+ }
2395
+ /**
2396
+ * Neural and keyword web search via Exa (Solana payment, $0.01/request).
2397
+ *
2398
+ * @example
2399
+ * const results = await client.exaSearch("latest AI papers", { numResults: 5 });
2400
+ */
2401
+ async exaSearch(query, options) {
2402
+ return this.requestWithPaymentRaw("/v1/exa/search", { query, ...options });
2403
+ }
2404
+ /**
2405
+ * Find pages semantically similar to a given URL via Exa (Solana payment, $0.01/request).
2406
+ *
2407
+ * @example
2408
+ * const results = await client.exaFindSimilar("https://openai.com/research/gpt-4", { numResults: 5 });
2409
+ */
2410
+ async exaFindSimilar(url, options) {
2411
+ return this.requestWithPaymentRaw("/v1/exa/find-similar", { url, ...options });
2412
+ }
2413
+ /**
2414
+ * Extract full text content from URLs via Exa (Solana payment, $0.002/URL).
2415
+ *
2416
+ * @example
2417
+ * const data = await client.exaContents(["https://arxiv.org/abs/2303.08774"]);
2418
+ */
2419
+ async exaContents(urls, options) {
2420
+ return this.requestWithPaymentRaw("/v1/exa/contents", { urls, ...options });
2421
+ }
2422
+ /**
2423
+ * AI-generated answer grounded in live web search via Exa (Solana payment, $0.01/request).
2424
+ *
2425
+ * @example
2426
+ * const answer = await client.exaAnswer("What is the current state of AI safety research?");
2427
+ */
2428
+ async exaAnswer(query, options) {
2429
+ return this.requestWithPaymentRaw("/v1/exa/answer", { query, ...options });
2430
+ }
1995
2431
  /** Get session spending. */
1996
2432
  getSpending() {
1997
2433
  return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
@@ -2293,6 +2729,8 @@ var path3 = __toESM(require("path"), 1);
2293
2729
  var os3 = __toESM(require("os"), 1);
2294
2730
  var crypto2 = __toESM(require("crypto"), 1);
2295
2731
  var CACHE_DIR = path3.join(os3.homedir(), ".blockrun", "cache");
2732
+ var DATA_DIR = path3.join(os3.homedir(), ".blockrun", "data");
2733
+ var COST_LOG_FILE = path3.join(os3.homedir(), ".blockrun", "cost_log.jsonl");
2296
2734
  var DEFAULT_TTL = {
2297
2735
  "/v1/x/": 3600 * 1e3,
2298
2736
  "/v1/partner/": 3600 * 1e3,
@@ -2357,26 +2795,80 @@ function setCache(key, data, ttlMs) {
2357
2795
  } catch {
2358
2796
  }
2359
2797
  }
2360
- function saveToCache(endpoint, body, response, costUsd = 0) {
2361
- const ttl = getTtl(endpoint);
2362
- if (ttl <= 0) return;
2798
+ function readableFilename(endpoint, body) {
2799
+ const now = /* @__PURE__ */ new Date();
2800
+ const ts = now.toISOString().slice(0, 10) + "_" + String(now.getHours()).padStart(2, "0") + String(now.getMinutes()).padStart(2, "0") + String(now.getSeconds()).padStart(2, "0");
2801
+ let ep = endpoint.replace(/\/+$/, "").split("/").pop() || "";
2802
+ if (endpoint.includes("/v1/chat/")) {
2803
+ ep = "chat";
2804
+ } else if (endpoint.includes("/v1/x/")) {
2805
+ ep = "x_" + ep;
2806
+ } else if (endpoint.includes("/v1/search")) {
2807
+ ep = "search";
2808
+ } else if (endpoint.includes("/v1/image")) {
2809
+ ep = "image";
2810
+ }
2811
+ let label = body.query || body.username || body.handle || body.model || (typeof body.prompt === "string" ? body.prompt.slice(0, 40) : "") || "";
2812
+ label = String(label).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40).replace(/^_+|_+$/g, "");
2813
+ return label ? `${ep}_${ts}_${label}.json` : `${ep}_${ts}.json`;
2814
+ }
2815
+ function saveReadable(endpoint, body, response, costUsd) {
2363
2816
  try {
2364
- fs3.mkdirSync(CACHE_DIR, { recursive: true });
2817
+ fs3.mkdirSync(DATA_DIR, { recursive: true });
2365
2818
  } catch {
2366
2819
  }
2367
- const key = cacheKey(endpoint, body);
2820
+ const filename = readableFilename(endpoint, body);
2368
2821
  const entry = {
2369
- cachedAt: Date.now(),
2822
+ saved_at: (/* @__PURE__ */ new Date()).toISOString(),
2370
2823
  endpoint,
2371
- body,
2372
- response,
2373
- costUsd
2824
+ cost_usd: costUsd,
2825
+ request: body,
2826
+ response
2374
2827
  };
2375
2828
  try {
2376
- fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
2829
+ fs3.writeFileSync(path3.join(DATA_DIR, filename), JSON.stringify(entry, null, 2));
2377
2830
  } catch {
2378
2831
  }
2379
2832
  }
2833
+ function appendCostLog(endpoint, costUsd) {
2834
+ if (costUsd <= 0) return;
2835
+ try {
2836
+ fs3.mkdirSync(path3.dirname(COST_LOG_FILE), { recursive: true });
2837
+ } catch {
2838
+ }
2839
+ const entry = {
2840
+ ts: Date.now() / 1e3,
2841
+ endpoint,
2842
+ cost_usd: costUsd
2843
+ };
2844
+ try {
2845
+ fs3.appendFileSync(COST_LOG_FILE, JSON.stringify(entry) + "\n");
2846
+ } catch {
2847
+ }
2848
+ }
2849
+ function saveToCache(endpoint, body, response, costUsd = 0) {
2850
+ const ttl = getTtl(endpoint);
2851
+ if (ttl > 0) {
2852
+ try {
2853
+ fs3.mkdirSync(CACHE_DIR, { recursive: true });
2854
+ } catch {
2855
+ }
2856
+ const key = cacheKey(endpoint, body);
2857
+ const entry = {
2858
+ cachedAt: Date.now(),
2859
+ endpoint,
2860
+ body,
2861
+ response,
2862
+ costUsd
2863
+ };
2864
+ try {
2865
+ fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
2866
+ } catch {
2867
+ }
2868
+ }
2869
+ saveReadable(endpoint, body, response, costUsd);
2870
+ appendCostLog(endpoint, costUsd);
2871
+ }
2380
2872
  function clearCache() {
2381
2873
  if (!fs3.existsSync(CACHE_DIR)) return 0;
2382
2874
  let count = 0;
@@ -2395,6 +2887,32 @@ function clearCache() {
2395
2887
  }
2396
2888
  return count;
2397
2889
  }
2890
+ function getCostLogSummary() {
2891
+ if (!fs3.existsSync(COST_LOG_FILE)) {
2892
+ return { totalUsd: 0, calls: 0, byEndpoint: {} };
2893
+ }
2894
+ let totalUsd = 0;
2895
+ let calls = 0;
2896
+ const byEndpoint = {};
2897
+ try {
2898
+ const content = fs3.readFileSync(COST_LOG_FILE, "utf-8").trim();
2899
+ if (!content) return { totalUsd: 0, calls: 0, byEndpoint: {} };
2900
+ for (const line of content.split("\n")) {
2901
+ if (!line) continue;
2902
+ try {
2903
+ const entry = JSON.parse(line);
2904
+ const cost = entry.cost_usd ?? 0;
2905
+ const ep = entry.endpoint ?? "unknown";
2906
+ totalUsd += cost;
2907
+ calls += 1;
2908
+ byEndpoint[ep] = (byEndpoint[ep] || 0) + cost;
2909
+ } catch {
2910
+ }
2911
+ }
2912
+ } catch {
2913
+ }
2914
+ return { totalUsd, calls, byEndpoint };
2915
+ }
2398
2916
 
2399
2917
  // src/setup.ts
2400
2918
  function setupAgentWallet(options) {
@@ -2436,27 +2954,27 @@ async function status() {
2436
2954
  var fs4 = __toESM(require("fs"), 1);
2437
2955
  var path4 = __toESM(require("path"), 1);
2438
2956
  var os4 = __toESM(require("os"), 1);
2439
- var DATA_DIR = path4.join(os4.homedir(), ".blockrun", "data");
2440
- var COST_LOG_FILE = path4.join(DATA_DIR, "costs.jsonl");
2957
+ var DATA_DIR2 = path4.join(os4.homedir(), ".blockrun", "data");
2958
+ var COST_LOG_FILE2 = path4.join(DATA_DIR2, "costs.jsonl");
2441
2959
  function logCost(entry) {
2442
2960
  try {
2443
- fs4.mkdirSync(DATA_DIR, { recursive: true });
2961
+ fs4.mkdirSync(DATA_DIR2, { recursive: true });
2444
2962
  } catch {
2445
2963
  }
2446
2964
  try {
2447
- fs4.appendFileSync(COST_LOG_FILE, JSON.stringify(entry) + "\n");
2965
+ fs4.appendFileSync(COST_LOG_FILE2, JSON.stringify(entry) + "\n");
2448
2966
  } catch {
2449
2967
  }
2450
2968
  }
2451
2969
  function getCostSummary() {
2452
- if (!fs4.existsSync(COST_LOG_FILE)) {
2970
+ if (!fs4.existsSync(COST_LOG_FILE2)) {
2453
2971
  return { totalUsd: 0, calls: 0, byModel: {} };
2454
2972
  }
2455
2973
  let totalUsd = 0;
2456
2974
  let calls = 0;
2457
2975
  const byModel = {};
2458
2976
  try {
2459
- const content = fs4.readFileSync(COST_LOG_FILE, "utf-8").trim();
2977
+ const content = fs4.readFileSync(COST_LOG_FILE2, "utf-8").trim();
2460
2978
  if (!content) return { totalUsd: 0, calls: 0, byModel: {} };
2461
2979
  for (const line of content.split("\n")) {
2462
2980
  if (!line) continue;
@@ -2555,46 +3073,18 @@ var ChatCompletions = class {
2555
3073
  return this.transformResponse(response);
2556
3074
  }
2557
3075
  async createStream(params) {
2558
- const url = `${this.apiUrl}/v1/chat/completions`;
2559
- const body = {
2560
- model: params.model,
2561
- messages: params.messages,
2562
- max_tokens: params.max_tokens || 1024,
2563
- temperature: params.temperature,
2564
- top_p: params.top_p,
2565
- stream: true
2566
- };
2567
- if (params.tools) {
2568
- body.tools = params.tools;
2569
- }
2570
- if (params.tool_choice) {
2571
- body.tool_choice = params.tool_choice;
2572
- }
2573
- const controller = new AbortController();
2574
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2575
- try {
2576
- const response = await fetch(url, {
2577
- method: "POST",
2578
- headers: { "Content-Type": "application/json" },
2579
- body: JSON.stringify(body),
2580
- signal: controller.signal
2581
- });
2582
- if (response.status === 402) {
2583
- const paymentHeader = response.headers.get("payment-required");
2584
- if (!paymentHeader) {
2585
- throw new Error("402 response but no payment requirements found");
2586
- }
2587
- throw new Error(
2588
- "Streaming with automatic payment requires direct wallet access. Please use non-streaming mode or contact support for streaming setup."
2589
- );
2590
- }
2591
- if (!response.ok) {
2592
- throw new Error(`API error: ${response.status}`);
3076
+ const response = await this.client.chatCompletionStream(
3077
+ params.model,
3078
+ params.messages,
3079
+ {
3080
+ maxTokens: params.max_tokens,
3081
+ temperature: params.temperature,
3082
+ topP: params.top_p,
3083
+ tools: params.tools,
3084
+ toolChoice: params.tool_choice
2593
3085
  }
2594
- return new StreamingResponse(response, params.model);
2595
- } finally {
2596
- clearTimeout(timeoutId);
2597
- }
3086
+ );
3087
+ return new StreamingResponse(response, params.model);
2598
3088
  }
2599
3089
  transformResponse(response) {
2600
3090
  return {
@@ -2758,6 +3248,7 @@ var AnthropicClient = class {
2758
3248
  BASE_CHAIN_ID,
2759
3249
  BlockrunError,
2760
3250
  ImageClient,
3251
+ KNOWN_PROVIDERS,
2761
3252
  LLMClient,
2762
3253
  OpenAI,
2763
3254
  PaymentError,
@@ -2780,6 +3271,7 @@ var AnthropicClient = class {
2780
3271
  formatWalletCreatedMessage,
2781
3272
  getCached,
2782
3273
  getCachedByRequest,
3274
+ getCostLogSummary,
2783
3275
  getCostSummary,
2784
3276
  getEip681Uri,
2785
3277
  getOrCreateSolanaWallet,
@@ -2802,5 +3294,9 @@ var AnthropicClient = class {
2802
3294
  solanaKeyToBytes,
2803
3295
  solanaPublicKey,
2804
3296
  status,
2805
- testnetClient
3297
+ testnetClient,
3298
+ validateMaxTokens,
3299
+ validateModel,
3300
+ validateTemperature,
3301
+ validateTopP
2806
3302
  });