@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.js CHANGED
@@ -223,6 +223,61 @@ function extractPaymentDetails(paymentRequired, preferredNetwork) {
223
223
 
224
224
  // src/validation.ts
225
225
  var LOCALHOST_DOMAINS = ["localhost", "127.0.0.1"];
226
+ var KNOWN_PROVIDERS = /* @__PURE__ */ new Set([
227
+ "openai",
228
+ "anthropic",
229
+ "google",
230
+ "deepseek",
231
+ "mistralai",
232
+ "meta-llama",
233
+ "together",
234
+ "xai",
235
+ "moonshot",
236
+ "nvidia",
237
+ "minimax",
238
+ "zai"
239
+ ]);
240
+ function validateModel(model) {
241
+ if (!model || typeof model !== "string") {
242
+ throw new Error("Model must be a non-empty string");
243
+ }
244
+ }
245
+ function validateMaxTokens(maxTokens) {
246
+ if (maxTokens === void 0 || maxTokens === null) {
247
+ return;
248
+ }
249
+ if (typeof maxTokens !== "number" || !Number.isInteger(maxTokens)) {
250
+ throw new Error("maxTokens must be an integer");
251
+ }
252
+ if (maxTokens < 1) {
253
+ throw new Error("maxTokens must be positive (minimum: 1)");
254
+ }
255
+ if (maxTokens > 1e5) {
256
+ throw new Error("maxTokens too large (maximum: 100000)");
257
+ }
258
+ }
259
+ function validateTemperature(temperature) {
260
+ if (temperature === void 0 || temperature === null) {
261
+ return;
262
+ }
263
+ if (typeof temperature !== "number") {
264
+ throw new Error("temperature must be a number");
265
+ }
266
+ if (temperature < 0 || temperature > 2) {
267
+ throw new Error("temperature must be between 0 and 2");
268
+ }
269
+ }
270
+ function validateTopP(topP) {
271
+ if (topP === void 0 || topP === null) {
272
+ return;
273
+ }
274
+ if (typeof topP !== "number") {
275
+ throw new Error("topP must be a number");
276
+ }
277
+ if (topP < 0 || topP > 1) {
278
+ throw new Error("topP must be between 0 and 1");
279
+ }
280
+ }
226
281
  function validatePrivateKey(key) {
227
282
  if (typeof key !== "string") {
228
283
  throw new Error("Private key must be a string");
@@ -298,9 +353,9 @@ var DEFAULT_API_URL = "https://blockrun.ai/api";
298
353
  var TESTNET_API_URL = "https://testnet.blockrun.ai/api";
299
354
  var DEFAULT_MAX_TOKENS = 1024;
300
355
  var DEFAULT_TIMEOUT = 6e4;
301
- var SDK_VERSION = "0.3.0";
356
+ var SDK_VERSION = "1.5.0";
302
357
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
303
- var LLMClient = class {
358
+ var LLMClient = class _LLMClient {
304
359
  static DEFAULT_API_URL = DEFAULT_API_URL;
305
360
  static TESTNET_API_URL = TESTNET_API_URL;
306
361
  account;
@@ -311,6 +366,11 @@ var LLMClient = class {
311
366
  sessionCalls = 0;
312
367
  modelPricingCache = null;
313
368
  modelPricingPromise = null;
369
+ // Pre-auth cache: avoids the 402 round-trip on repeat requests to the same model.
370
+ // Key = "endpoint:model", value = cached payment header + timestamp.
371
+ // TTL: 1 hour (mirrors ClawRouter's payment-preauth.ts approach).
372
+ preAuthCache = /* @__PURE__ */ new Map();
373
+ static PRE_AUTH_TTL_MS = 36e5;
314
374
  /**
315
375
  * Initialize the BlockRun LLM client.
316
376
  *
@@ -335,13 +395,13 @@ var LLMClient = class {
335
395
  /**
336
396
  * Simple 1-line chat interface.
337
397
  *
338
- * @param model - Model ID (e.g., 'openai/gpt-4o', 'anthropic/claude-sonnet-4')
398
+ * @param model - Model ID (e.g., 'openai/gpt-5.2', 'anthropic/claude-sonnet-4.6')
339
399
  * @param prompt - User message
340
400
  * @param options - Optional chat parameters
341
401
  * @returns Assistant's response text
342
402
  *
343
403
  * @example
344
- * const response = await client.chat('gpt-4o', 'What is the capital of France?');
404
+ * const response = await client.chat('gpt-5.2', 'What is the capital of France?');
345
405
  * console.log(response); // 'The capital of France is Paris.'
346
406
  */
347
407
  async chat(model, prompt, options) {
@@ -487,6 +547,27 @@ var LLMClient = class {
487
547
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
488
548
  body: JSON.stringify(body)
489
549
  });
550
+ if (response.status === 502 || response.status === 503) {
551
+ await new Promise((r) => setTimeout(r, 1e3));
552
+ const retryResp = await this.fetchWithTimeout(url, {
553
+ method: "POST",
554
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
555
+ body: JSON.stringify(body)
556
+ });
557
+ if (retryResp.status !== 502 && retryResp.status !== 503) {
558
+ if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp);
559
+ if (!retryResp.ok) {
560
+ let errorBody;
561
+ try {
562
+ errorBody = await retryResp.json();
563
+ } catch {
564
+ errorBody = { error: "Request failed" };
565
+ }
566
+ throw new APIError(`API error: ${retryResp.status}`, retryResp.status, sanitizeErrorResponse(errorBody));
567
+ }
568
+ return retryResp.json();
569
+ }
570
+ }
490
571
  if (response.status === 402) {
491
572
  return this.handlePaymentAndRetry(url, body, response);
492
573
  }
@@ -552,6 +633,34 @@ var LLMClient = class {
552
633
  },
553
634
  body: JSON.stringify(body)
554
635
  });
636
+ if (retryResponse.status === 502 || retryResponse.status === 503) {
637
+ await new Promise((r) => setTimeout(r, 1e3));
638
+ const retryResp2 = await this.fetchWithTimeout(url, {
639
+ method: "POST",
640
+ headers: {
641
+ "Content-Type": "application/json",
642
+ "User-Agent": USER_AGENT,
643
+ "PAYMENT-SIGNATURE": paymentPayload
644
+ },
645
+ body: JSON.stringify(body)
646
+ });
647
+ if (retryResp2.status !== 502 && retryResp2.status !== 503) {
648
+ if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
649
+ if (!retryResp2.ok) {
650
+ let errorBody;
651
+ try {
652
+ errorBody = await retryResp2.json();
653
+ } catch {
654
+ errorBody = { error: "Request failed" };
655
+ }
656
+ throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
657
+ }
658
+ const costUsd2 = parseFloat(details.amount) / 1e6;
659
+ this.sessionCalls += 1;
660
+ this.sessionTotalUsd += costUsd2;
661
+ return retryResp2.json();
662
+ }
663
+ }
555
664
  if (retryResponse.status === 402) {
556
665
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
557
666
  }
@@ -573,6 +682,131 @@ var LLMClient = class {
573
682
  this.sessionTotalUsd += costUsd;
574
683
  return retryResponse.json();
575
684
  }
685
+ /**
686
+ * Sign a payment header and return the PAYMENT-SIGNATURE value.
687
+ * Extracted to share logic between streaming and non-streaming flows.
688
+ */
689
+ async signPayment(paymentHeader) {
690
+ const paymentRequired = parsePaymentRequired(paymentHeader);
691
+ const details = extractPaymentDetails(paymentRequired);
692
+ const extensions = paymentRequired.extensions;
693
+ const paymentPayload = await createPaymentPayload(
694
+ this.privateKey,
695
+ this.account.address,
696
+ details.recipient,
697
+ details.amount,
698
+ details.network || "eip155:8453",
699
+ {
700
+ resourceUrl: validateResourceUrl(
701
+ details.resource?.url || `${this.apiUrl}/v1/chat/completions`,
702
+ this.apiUrl
703
+ ),
704
+ resourceDescription: details.resource?.description || "BlockRun AI API call",
705
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
706
+ extra: details.extra,
707
+ extensions
708
+ }
709
+ );
710
+ const costUsd = parseFloat(details.amount) / 1e6;
711
+ return { paymentPayload, costUsd };
712
+ }
713
+ /**
714
+ * Streaming chat completion with automatic x402 payment.
715
+ *
716
+ * Uses a pre-auth cache so repeat calls to the same model skip the 402
717
+ * round-trip (~200ms savings). Falls back to the normal 402 flow on cache
718
+ * miss or if the pre-signed payment is rejected.
719
+ *
720
+ * @returns Raw fetch Response with a streaming SSE body.
721
+ */
722
+ async chatCompletionStream(model, messages, options) {
723
+ const url = `${this.apiUrl}/v1/chat/completions`;
724
+ const body = {
725
+ model,
726
+ messages,
727
+ max_tokens: options?.maxTokens ?? DEFAULT_MAX_TOKENS,
728
+ stream: true
729
+ };
730
+ if (options?.temperature !== void 0) body.temperature = options.temperature;
731
+ if (options?.topP !== void 0) body.top_p = options.topP;
732
+ if (options?.tools !== void 0) body.tools = options.tools;
733
+ if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
734
+ const cacheKey2 = `/v1/chat/completions:${model}`;
735
+ const cached = this.preAuthCache.get(cacheKey2);
736
+ const now = Date.now();
737
+ if (cached && now - cached.cachedAt < _LLMClient.PRE_AUTH_TTL_MS) {
738
+ try {
739
+ const { paymentPayload: paymentPayload2, costUsd: costUsd2 } = await this.signPayment(cached.paymentHeader);
740
+ const preAuthResp = await this.fetchWithTimeout(url, {
741
+ method: "POST",
742
+ headers: {
743
+ "Content-Type": "application/json",
744
+ "User-Agent": USER_AGENT,
745
+ "PAYMENT-SIGNATURE": paymentPayload2
746
+ },
747
+ body: JSON.stringify(body)
748
+ });
749
+ if (preAuthResp.status !== 402 && preAuthResp.ok) {
750
+ this.sessionCalls += 1;
751
+ this.sessionTotalUsd += costUsd2;
752
+ return preAuthResp;
753
+ }
754
+ this.preAuthCache.delete(cacheKey2);
755
+ } catch {
756
+ this.preAuthCache.delete(cacheKey2);
757
+ }
758
+ }
759
+ const firstResp = await this.fetchWithTimeout(url, {
760
+ method: "POST",
761
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
762
+ body: JSON.stringify(body)
763
+ });
764
+ if (firstResp.status !== 402) {
765
+ if (!firstResp.ok) {
766
+ let errorBody;
767
+ try {
768
+ errorBody = await firstResp.json();
769
+ } catch {
770
+ errorBody = { error: "Request failed" };
771
+ }
772
+ throw new APIError(`API error: ${firstResp.status}`, firstResp.status, sanitizeErrorResponse(errorBody));
773
+ }
774
+ return firstResp;
775
+ }
776
+ let paymentHeader = firstResp.headers.get("payment-required");
777
+ if (!paymentHeader) {
778
+ try {
779
+ const rb = await firstResp.json();
780
+ if (rb.x402 || rb.accepts) paymentHeader = btoa(JSON.stringify(rb));
781
+ } catch {
782
+ }
783
+ }
784
+ if (!paymentHeader) throw new PaymentError("402 response but no payment requirements found");
785
+ this.preAuthCache.set(cacheKey2, { paymentHeader, cachedAt: now });
786
+ const { paymentPayload, costUsd } = await this.signPayment(paymentHeader);
787
+ const streamResp = await this.fetchWithTimeout(url, {
788
+ method: "POST",
789
+ headers: {
790
+ "Content-Type": "application/json",
791
+ "User-Agent": USER_AGENT,
792
+ "PAYMENT-SIGNATURE": paymentPayload
793
+ },
794
+ body: JSON.stringify(body)
795
+ });
796
+ if (streamResp.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
797
+ if (!streamResp.ok) {
798
+ let errorBody;
799
+ try {
800
+ errorBody = await streamResp.json();
801
+ } catch {
802
+ errorBody = { error: "Request failed" };
803
+ }
804
+ throw new APIError(`API error after payment: ${streamResp.status}`, streamResp.status, sanitizeErrorResponse(errorBody));
805
+ }
806
+ this.sessionCalls += 1;
807
+ this.sessionTotalUsd += costUsd;
808
+ return streamResp;
809
+ }
576
810
  /**
577
811
  * Make a request with automatic x402 payment handling, returning raw JSON.
578
812
  * Used for non-ChatResponse endpoints (X/Twitter, search, image edit, etc.).
@@ -584,6 +818,27 @@ var LLMClient = class {
584
818
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
585
819
  body: JSON.stringify(body)
586
820
  });
821
+ if (response.status === 502 || response.status === 503) {
822
+ await new Promise((r) => setTimeout(r, 1e3));
823
+ const retryResp = await this.fetchWithTimeout(url, {
824
+ method: "POST",
825
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
826
+ body: JSON.stringify(body)
827
+ });
828
+ if (retryResp.status !== 502 && retryResp.status !== 503) {
829
+ if (retryResp.status === 402) return this.handlePaymentAndRetryRaw(url, body, retryResp);
830
+ if (!retryResp.ok) {
831
+ let errorBody;
832
+ try {
833
+ errorBody = await retryResp.json();
834
+ } catch {
835
+ errorBody = { error: "Request failed" };
836
+ }
837
+ throw new APIError(`API error: ${retryResp.status}`, retryResp.status, sanitizeErrorResponse(errorBody));
838
+ }
839
+ return retryResp.json();
840
+ }
841
+ }
587
842
  if (response.status === 402) {
588
843
  return this.handlePaymentAndRetryRaw(url, body, response);
589
844
  }
@@ -649,6 +904,34 @@ var LLMClient = class {
649
904
  },
650
905
  body: JSON.stringify(body)
651
906
  });
907
+ if (retryResponse.status === 502 || retryResponse.status === 503) {
908
+ await new Promise((r) => setTimeout(r, 1e3));
909
+ const retryResp2 = await this.fetchWithTimeout(url, {
910
+ method: "POST",
911
+ headers: {
912
+ "Content-Type": "application/json",
913
+ "User-Agent": USER_AGENT,
914
+ "PAYMENT-SIGNATURE": paymentPayload
915
+ },
916
+ body: JSON.stringify(body)
917
+ });
918
+ if (retryResp2.status !== 502 && retryResp2.status !== 503) {
919
+ if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
920
+ if (!retryResp2.ok) {
921
+ let errorBody;
922
+ try {
923
+ errorBody = await retryResp2.json();
924
+ } catch {
925
+ errorBody = { error: "Request failed" };
926
+ }
927
+ throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
928
+ }
929
+ const costUsd2 = parseFloat(details.amount) / 1e6;
930
+ this.sessionCalls += 1;
931
+ this.sessionTotalUsd += costUsd2;
932
+ return retryResp2.json();
933
+ }
934
+ }
652
935
  if (retryResponse.status === 402) {
653
936
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
654
937
  }
@@ -681,6 +964,26 @@ var LLMClient = class {
681
964
  method: "GET",
682
965
  headers: { "User-Agent": USER_AGENT }
683
966
  });
967
+ if (response.status === 502 || response.status === 503) {
968
+ await new Promise((r) => setTimeout(r, 1e3));
969
+ const retryResp = await this.fetchWithTimeout(url, {
970
+ method: "GET",
971
+ headers: { "User-Agent": USER_AGENT }
972
+ });
973
+ if (retryResp.status !== 502 && retryResp.status !== 503) {
974
+ if (retryResp.status === 402) return this.handleGetPaymentAndRetryRaw(url, endpoint, params, retryResp);
975
+ if (!retryResp.ok) {
976
+ let errorBody;
977
+ try {
978
+ errorBody = await retryResp.json();
979
+ } catch {
980
+ errorBody = { error: "Request failed" };
981
+ }
982
+ throw new APIError(`API error: ${retryResp.status}`, retryResp.status, sanitizeErrorResponse(errorBody));
983
+ }
984
+ return retryResp.json();
985
+ }
986
+ }
684
987
  if (response.status === 402) {
685
988
  return this.handleGetPaymentAndRetryRaw(url, endpoint, params, response);
686
989
  }
@@ -746,6 +1049,32 @@ var LLMClient = class {
746
1049
  "PAYMENT-SIGNATURE": paymentPayload
747
1050
  }
748
1051
  });
1052
+ if (retryResponse.status === 502 || retryResponse.status === 503) {
1053
+ await new Promise((r) => setTimeout(r, 1e3));
1054
+ const retryResp2 = await this.fetchWithTimeout(retryUrl, {
1055
+ method: "GET",
1056
+ headers: {
1057
+ "User-Agent": USER_AGENT,
1058
+ "PAYMENT-SIGNATURE": paymentPayload
1059
+ }
1060
+ });
1061
+ if (retryResp2.status !== 502 && retryResp2.status !== 503) {
1062
+ if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
1063
+ if (!retryResp2.ok) {
1064
+ let errorBody;
1065
+ try {
1066
+ errorBody = await retryResp2.json();
1067
+ } catch {
1068
+ errorBody = { error: "Request failed" };
1069
+ }
1070
+ throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
1071
+ }
1072
+ const costUsd2 = parseFloat(details.amount) / 1e6;
1073
+ this.sessionCalls += 1;
1074
+ this.sessionTotalUsd += costUsd2;
1075
+ return retryResp2.json();
1076
+ }
1077
+ }
749
1078
  if (retryResponse.status === 402) {
750
1079
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
751
1080
  }
@@ -807,12 +1136,12 @@ var LLMClient = class {
807
1136
  return (data.data || []).map((m) => ({
808
1137
  id: m.id,
809
1138
  name: m.name || m.id,
810
- provider: m.owned_by || "",
1139
+ provider: m.provider || m.owned_by || "",
811
1140
  description: m.description || "",
812
- inputPrice: m.pricing?.input ?? m.pricing?.flat ?? 0,
813
- outputPrice: m.pricing?.output ?? 0,
814
- contextWindow: m.context_window || 0,
815
- maxOutput: m.max_output || 0,
1141
+ inputPrice: m.inputPrice ?? m.input_price ?? m.pricing?.input ?? 0,
1142
+ outputPrice: m.outputPrice ?? m.output_price ?? m.pricing?.output ?? 0,
1143
+ contextWindow: m.contextWindow ?? m.context_window ?? 0,
1144
+ maxOutput: m.maxOutput ?? m.max_output ?? 0,
816
1145
  categories: m.categories || [],
817
1146
  available: true
818
1147
  }));
@@ -900,6 +1229,58 @@ var LLMClient = class {
900
1229
  const data = await this.requestWithPaymentRaw("/v1/search", body);
901
1230
  return data;
902
1231
  }
1232
+ /**
1233
+ * Neural web search via Exa. Returns semantically relevant URLs and metadata.
1234
+ * Understands meaning, not just keywords. $0.01/call.
1235
+ *
1236
+ * @param query - Natural language search query
1237
+ * @param options - Optional filters (numResults, category, date range, domains)
1238
+ */
1239
+ async exaSearch(query, options) {
1240
+ const body = { query };
1241
+ if (options?.numResults !== void 0) body.numResults = options.numResults;
1242
+ if (options?.category !== void 0) body.category = options.category;
1243
+ if (options?.startPublishedDate !== void 0) body.startPublishedDate = options.startPublishedDate;
1244
+ if (options?.endPublishedDate !== void 0) body.endPublishedDate = options.endPublishedDate;
1245
+ if (options?.includeDomains !== void 0) body.includeDomains = options.includeDomains;
1246
+ if (options?.excludeDomains !== void 0) body.excludeDomains = options.excludeDomains;
1247
+ const data = await this.requestWithPaymentRaw("/v1/exa/search", body);
1248
+ return data.data;
1249
+ }
1250
+ /**
1251
+ * Ask a question and get a cited, synthesized answer grounded in real web sources.
1252
+ * No hallucinations — every claim is backed by a citation. $0.01/call.
1253
+ *
1254
+ * @param query - The question to answer
1255
+ */
1256
+ async exaAnswer(query) {
1257
+ const data = await this.requestWithPaymentRaw("/v1/exa/answer", { query });
1258
+ return data.data;
1259
+ }
1260
+ /**
1261
+ * Fetch full Markdown text content from a list of URLs. $0.002 per URL.
1262
+ * Returns clean text ready to feed into an LLM context window.
1263
+ *
1264
+ * @param urls - Array of URLs to fetch (up to 100)
1265
+ */
1266
+ async exaContents(urls) {
1267
+ const data = await this.requestWithPaymentRaw("/v1/exa/contents", { urls });
1268
+ return data.data;
1269
+ }
1270
+ /**
1271
+ * Find pages semantically similar to a given URL. $0.01/call.
1272
+ * Useful for discovering competitors, alternatives, and related resources.
1273
+ *
1274
+ * @param url - Reference URL
1275
+ * @param options - Optional filters (numResults, excludeSourceDomain)
1276
+ */
1277
+ async exaFindSimilar(url, options) {
1278
+ const body = { url };
1279
+ if (options?.numResults !== void 0) body.numResults = options.numResults;
1280
+ if (options?.excludeSourceDomain !== void 0) body.excludeSourceDomain = options.excludeSourceDomain;
1281
+ const data = await this.requestWithPaymentRaw("/v1/exa/find-similar", body);
1282
+ return data.data;
1283
+ }
903
1284
  /**
904
1285
  * Get USDC balance on Base network.
905
1286
  *
@@ -1913,6 +2294,55 @@ var SolanaLLMClient = class {
1913
2294
  async pmQuery(path5, query) {
1914
2295
  return this.requestWithPaymentRaw(`/v1/pm/${path5}`, query);
1915
2296
  }
2297
+ // ── Exa Web Search (Powered by Exa) ──────────────────────────────────────
2298
+ /**
2299
+ * Generic Exa endpoint proxy (POST, Solana payment). Powered by Exa.
2300
+ *
2301
+ * @param path - Exa endpoint: "search" | "find-similar" | "contents" | "answer"
2302
+ * @param body - Request body (see Exa API docs)
2303
+ *
2304
+ * @example
2305
+ * const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
2306
+ */
2307
+ async exa(path5, body) {
2308
+ return this.requestWithPaymentRaw(`/v1/exa/${path5}`, body);
2309
+ }
2310
+ /**
2311
+ * Neural and keyword web search via Exa (Solana payment, $0.01/request).
2312
+ *
2313
+ * @example
2314
+ * const results = await client.exaSearch("latest AI papers", { numResults: 5 });
2315
+ */
2316
+ async exaSearch(query, options) {
2317
+ return this.requestWithPaymentRaw("/v1/exa/search", { query, ...options });
2318
+ }
2319
+ /**
2320
+ * Find pages semantically similar to a given URL via Exa (Solana payment, $0.01/request).
2321
+ *
2322
+ * @example
2323
+ * const results = await client.exaFindSimilar("https://openai.com/research/gpt-4", { numResults: 5 });
2324
+ */
2325
+ async exaFindSimilar(url, options) {
2326
+ return this.requestWithPaymentRaw("/v1/exa/find-similar", { url, ...options });
2327
+ }
2328
+ /**
2329
+ * Extract full text content from URLs via Exa (Solana payment, $0.002/URL).
2330
+ *
2331
+ * @example
2332
+ * const data = await client.exaContents(["https://arxiv.org/abs/2303.08774"]);
2333
+ */
2334
+ async exaContents(urls, options) {
2335
+ return this.requestWithPaymentRaw("/v1/exa/contents", { urls, ...options });
2336
+ }
2337
+ /**
2338
+ * AI-generated answer grounded in live web search via Exa (Solana payment, $0.01/request).
2339
+ *
2340
+ * @example
2341
+ * const answer = await client.exaAnswer("What is the current state of AI safety research?");
2342
+ */
2343
+ async exaAnswer(query, options) {
2344
+ return this.requestWithPaymentRaw("/v1/exa/answer", { query, ...options });
2345
+ }
1916
2346
  /** Get session spending. */
1917
2347
  getSpending() {
1918
2348
  return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
@@ -2214,6 +2644,8 @@ import * as path3 from "path";
2214
2644
  import * as os3 from "os";
2215
2645
  import * as crypto2 from "crypto";
2216
2646
  var CACHE_DIR = path3.join(os3.homedir(), ".blockrun", "cache");
2647
+ var DATA_DIR = path3.join(os3.homedir(), ".blockrun", "data");
2648
+ var COST_LOG_FILE = path3.join(os3.homedir(), ".blockrun", "cost_log.jsonl");
2217
2649
  var DEFAULT_TTL = {
2218
2650
  "/v1/x/": 3600 * 1e3,
2219
2651
  "/v1/partner/": 3600 * 1e3,
@@ -2278,26 +2710,80 @@ function setCache(key, data, ttlMs) {
2278
2710
  } catch {
2279
2711
  }
2280
2712
  }
2281
- function saveToCache(endpoint, body, response, costUsd = 0) {
2282
- const ttl = getTtl(endpoint);
2283
- if (ttl <= 0) return;
2713
+ function readableFilename(endpoint, body) {
2714
+ const now = /* @__PURE__ */ new Date();
2715
+ 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");
2716
+ let ep = endpoint.replace(/\/+$/, "").split("/").pop() || "";
2717
+ if (endpoint.includes("/v1/chat/")) {
2718
+ ep = "chat";
2719
+ } else if (endpoint.includes("/v1/x/")) {
2720
+ ep = "x_" + ep;
2721
+ } else if (endpoint.includes("/v1/search")) {
2722
+ ep = "search";
2723
+ } else if (endpoint.includes("/v1/image")) {
2724
+ ep = "image";
2725
+ }
2726
+ let label = body.query || body.username || body.handle || body.model || (typeof body.prompt === "string" ? body.prompt.slice(0, 40) : "") || "";
2727
+ label = String(label).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40).replace(/^_+|_+$/g, "");
2728
+ return label ? `${ep}_${ts}_${label}.json` : `${ep}_${ts}.json`;
2729
+ }
2730
+ function saveReadable(endpoint, body, response, costUsd) {
2284
2731
  try {
2285
- fs3.mkdirSync(CACHE_DIR, { recursive: true });
2732
+ fs3.mkdirSync(DATA_DIR, { recursive: true });
2286
2733
  } catch {
2287
2734
  }
2288
- const key = cacheKey(endpoint, body);
2735
+ const filename = readableFilename(endpoint, body);
2289
2736
  const entry = {
2290
- cachedAt: Date.now(),
2737
+ saved_at: (/* @__PURE__ */ new Date()).toISOString(),
2291
2738
  endpoint,
2292
- body,
2293
- response,
2294
- costUsd
2739
+ cost_usd: costUsd,
2740
+ request: body,
2741
+ response
2295
2742
  };
2296
2743
  try {
2297
- fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
2744
+ fs3.writeFileSync(path3.join(DATA_DIR, filename), JSON.stringify(entry, null, 2));
2298
2745
  } catch {
2299
2746
  }
2300
2747
  }
2748
+ function appendCostLog(endpoint, costUsd) {
2749
+ if (costUsd <= 0) return;
2750
+ try {
2751
+ fs3.mkdirSync(path3.dirname(COST_LOG_FILE), { recursive: true });
2752
+ } catch {
2753
+ }
2754
+ const entry = {
2755
+ ts: Date.now() / 1e3,
2756
+ endpoint,
2757
+ cost_usd: costUsd
2758
+ };
2759
+ try {
2760
+ fs3.appendFileSync(COST_LOG_FILE, JSON.stringify(entry) + "\n");
2761
+ } catch {
2762
+ }
2763
+ }
2764
+ function saveToCache(endpoint, body, response, costUsd = 0) {
2765
+ const ttl = getTtl(endpoint);
2766
+ if (ttl > 0) {
2767
+ try {
2768
+ fs3.mkdirSync(CACHE_DIR, { recursive: true });
2769
+ } catch {
2770
+ }
2771
+ const key = cacheKey(endpoint, body);
2772
+ const entry = {
2773
+ cachedAt: Date.now(),
2774
+ endpoint,
2775
+ body,
2776
+ response,
2777
+ costUsd
2778
+ };
2779
+ try {
2780
+ fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
2781
+ } catch {
2782
+ }
2783
+ }
2784
+ saveReadable(endpoint, body, response, costUsd);
2785
+ appendCostLog(endpoint, costUsd);
2786
+ }
2301
2787
  function clearCache() {
2302
2788
  if (!fs3.existsSync(CACHE_DIR)) return 0;
2303
2789
  let count = 0;
@@ -2316,6 +2802,32 @@ function clearCache() {
2316
2802
  }
2317
2803
  return count;
2318
2804
  }
2805
+ function getCostLogSummary() {
2806
+ if (!fs3.existsSync(COST_LOG_FILE)) {
2807
+ return { totalUsd: 0, calls: 0, byEndpoint: {} };
2808
+ }
2809
+ let totalUsd = 0;
2810
+ let calls = 0;
2811
+ const byEndpoint = {};
2812
+ try {
2813
+ const content = fs3.readFileSync(COST_LOG_FILE, "utf-8").trim();
2814
+ if (!content) return { totalUsd: 0, calls: 0, byEndpoint: {} };
2815
+ for (const line of content.split("\n")) {
2816
+ if (!line) continue;
2817
+ try {
2818
+ const entry = JSON.parse(line);
2819
+ const cost = entry.cost_usd ?? 0;
2820
+ const ep = entry.endpoint ?? "unknown";
2821
+ totalUsd += cost;
2822
+ calls += 1;
2823
+ byEndpoint[ep] = (byEndpoint[ep] || 0) + cost;
2824
+ } catch {
2825
+ }
2826
+ }
2827
+ } catch {
2828
+ }
2829
+ return { totalUsd, calls, byEndpoint };
2830
+ }
2319
2831
 
2320
2832
  // src/setup.ts
2321
2833
  function setupAgentWallet(options) {
@@ -2357,27 +2869,27 @@ async function status() {
2357
2869
  import * as fs4 from "fs";
2358
2870
  import * as path4 from "path";
2359
2871
  import * as os4 from "os";
2360
- var DATA_DIR = path4.join(os4.homedir(), ".blockrun", "data");
2361
- var COST_LOG_FILE = path4.join(DATA_DIR, "costs.jsonl");
2872
+ var DATA_DIR2 = path4.join(os4.homedir(), ".blockrun", "data");
2873
+ var COST_LOG_FILE2 = path4.join(DATA_DIR2, "costs.jsonl");
2362
2874
  function logCost(entry) {
2363
2875
  try {
2364
- fs4.mkdirSync(DATA_DIR, { recursive: true });
2876
+ fs4.mkdirSync(DATA_DIR2, { recursive: true });
2365
2877
  } catch {
2366
2878
  }
2367
2879
  try {
2368
- fs4.appendFileSync(COST_LOG_FILE, JSON.stringify(entry) + "\n");
2880
+ fs4.appendFileSync(COST_LOG_FILE2, JSON.stringify(entry) + "\n");
2369
2881
  } catch {
2370
2882
  }
2371
2883
  }
2372
2884
  function getCostSummary() {
2373
- if (!fs4.existsSync(COST_LOG_FILE)) {
2885
+ if (!fs4.existsSync(COST_LOG_FILE2)) {
2374
2886
  return { totalUsd: 0, calls: 0, byModel: {} };
2375
2887
  }
2376
2888
  let totalUsd = 0;
2377
2889
  let calls = 0;
2378
2890
  const byModel = {};
2379
2891
  try {
2380
- const content = fs4.readFileSync(COST_LOG_FILE, "utf-8").trim();
2892
+ const content = fs4.readFileSync(COST_LOG_FILE2, "utf-8").trim();
2381
2893
  if (!content) return { totalUsd: 0, calls: 0, byModel: {} };
2382
2894
  for (const line of content.split("\n")) {
2383
2895
  if (!line) continue;
@@ -2476,46 +2988,18 @@ var ChatCompletions = class {
2476
2988
  return this.transformResponse(response);
2477
2989
  }
2478
2990
  async createStream(params) {
2479
- const url = `${this.apiUrl}/v1/chat/completions`;
2480
- const body = {
2481
- model: params.model,
2482
- messages: params.messages,
2483
- max_tokens: params.max_tokens || 1024,
2484
- temperature: params.temperature,
2485
- top_p: params.top_p,
2486
- stream: true
2487
- };
2488
- if (params.tools) {
2489
- body.tools = params.tools;
2490
- }
2491
- if (params.tool_choice) {
2492
- body.tool_choice = params.tool_choice;
2493
- }
2494
- const controller = new AbortController();
2495
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2496
- try {
2497
- const response = await fetch(url, {
2498
- method: "POST",
2499
- headers: { "Content-Type": "application/json" },
2500
- body: JSON.stringify(body),
2501
- signal: controller.signal
2502
- });
2503
- if (response.status === 402) {
2504
- const paymentHeader = response.headers.get("payment-required");
2505
- if (!paymentHeader) {
2506
- throw new Error("402 response but no payment requirements found");
2507
- }
2508
- throw new Error(
2509
- "Streaming with automatic payment requires direct wallet access. Please use non-streaming mode or contact support for streaming setup."
2510
- );
2511
- }
2512
- if (!response.ok) {
2513
- throw new Error(`API error: ${response.status}`);
2991
+ const response = await this.client.chatCompletionStream(
2992
+ params.model,
2993
+ params.messages,
2994
+ {
2995
+ maxTokens: params.max_tokens,
2996
+ temperature: params.temperature,
2997
+ topP: params.top_p,
2998
+ tools: params.tools,
2999
+ toolChoice: params.tool_choice
2514
3000
  }
2515
- return new StreamingResponse(response, params.model);
2516
- } finally {
2517
- clearTimeout(timeoutId);
2518
- }
3001
+ );
3002
+ return new StreamingResponse(response, params.model);
2519
3003
  }
2520
3004
  transformResponse(response) {
2521
3005
  return {
@@ -2678,6 +3162,7 @@ export {
2678
3162
  BASE_CHAIN_ID,
2679
3163
  BlockrunError,
2680
3164
  ImageClient,
3165
+ KNOWN_PROVIDERS,
2681
3166
  LLMClient,
2682
3167
  OpenAI,
2683
3168
  PaymentError,
@@ -2701,6 +3186,7 @@ export {
2701
3186
  formatWalletCreatedMessage,
2702
3187
  getCached,
2703
3188
  getCachedByRequest,
3189
+ getCostLogSummary,
2704
3190
  getCostSummary,
2705
3191
  getEip681Uri,
2706
3192
  getOrCreateSolanaWallet,
@@ -2723,5 +3209,9 @@ export {
2723
3209
  solanaKeyToBytes,
2724
3210
  solanaPublicKey,
2725
3211
  status,
2726
- testnetClient
3212
+ testnetClient,
3213
+ validateMaxTokens,
3214
+ validateModel,
3215
+ validateTemperature,
3216
+ validateTopP
2727
3217
  };