@blockrun/llm 1.5.0 → 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/README.md CHANGED
@@ -550,6 +550,46 @@ All current endpoints are GET. The `pmQuery()` method is available for future PO
550
550
 
551
551
  Works on both `LLMClient` (Base) and `SolanaLLMClient`.
552
552
 
553
+ ## Exa Web Search (Powered by Exa)
554
+
555
+ Access [Exa](https://exa.ai)'s neural web search via x402. No API keys needed — pay-per-request via Solana USDC. Available on `SolanaLLMClient` only.
556
+
557
+ | Method | Description | Price |
558
+ |---|---|---|
559
+ | `exaSearch(query, options?)` | Neural/keyword web search | $0.01/request |
560
+ | `exaFindSimilar(url, options?)` | Find semantically similar pages | $0.01/request |
561
+ | `exaContents(urls, options?)` | Extract full text from URLs | $0.002/URL |
562
+ | `exaAnswer(query, options?)` | AI answer grounded in web search | $0.01/request |
563
+ | `exa(path, body)` | Generic proxy for any Exa endpoint | varies |
564
+
565
+ ```typescript
566
+ import { SolanaLLMClient } from '@blockrun/llm';
567
+
568
+ const client = new SolanaLLMClient();
569
+
570
+ // Neural web search ($0.01/request)
571
+ const results = await client.exaSearch("latest AI safety research", { numResults: 5 });
572
+ const news = await client.exaSearch("bitcoin ETF news", { category: "news", numResults: 10 });
573
+
574
+ // Find similar pages ($0.01/request)
575
+ const similar = await client.exaFindSimilar("https://openai.com/research/gpt-4", { numResults: 5 });
576
+
577
+ // Extract content from URLs ($0.002/URL)
578
+ const content = await client.exaContents(["https://arxiv.org/abs/2303.08774"]);
579
+ const rich = await client.exaContents(
580
+ ["https://example.com/page1", "https://example.com/page2"],
581
+ { text: true, highlights: true }
582
+ );
583
+
584
+ // AI-generated answer from live web ($0.01/request)
585
+ const answer = await client.exaAnswer("What is the current state of AI safety research?");
586
+
587
+ // Generic proxy for any Exa endpoint
588
+ const custom = await client.exa("search", { query: "transformer architecture", numResults: 5 });
589
+ ```
590
+
591
+ `SolanaLLMClient` only — Exa endpoints are on `sol.blockrun.ai`.
592
+
553
593
  ## Configuration
554
594
 
555
595
  ```typescript
package/dist/index.cjs CHANGED
@@ -440,7 +440,7 @@ var DEFAULT_MAX_TOKENS = 1024;
440
440
  var DEFAULT_TIMEOUT = 6e4;
441
441
  var SDK_VERSION = "1.5.0";
442
442
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
443
- var LLMClient = class {
443
+ var LLMClient = class _LLMClient {
444
444
  static DEFAULT_API_URL = DEFAULT_API_URL;
445
445
  static TESTNET_API_URL = TESTNET_API_URL;
446
446
  account;
@@ -451,6 +451,11 @@ var LLMClient = class {
451
451
  sessionCalls = 0;
452
452
  modelPricingCache = null;
453
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;
454
459
  /**
455
460
  * Initialize the BlockRun LLM client.
456
461
  *
@@ -762,6 +767,131 @@ var LLMClient = class {
762
767
  this.sessionTotalUsd += costUsd;
763
768
  return retryResponse.json();
764
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
+ }
765
895
  /**
766
896
  * Make a request with automatic x402 payment handling, returning raw JSON.
767
897
  * Used for non-ChatResponse endpoints (X/Twitter, search, image edit, etc.).
@@ -1091,12 +1221,12 @@ var LLMClient = class {
1091
1221
  return (data.data || []).map((m) => ({
1092
1222
  id: m.id,
1093
1223
  name: m.name || m.id,
1094
- provider: m.owned_by || "",
1224
+ provider: m.provider || m.owned_by || "",
1095
1225
  description: m.description || "",
1096
- inputPrice: m.pricing?.input ?? m.pricing?.flat ?? 0,
1097
- outputPrice: m.pricing?.output ?? 0,
1098
- contextWindow: m.context_window || 0,
1099
- 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,
1100
1230
  categories: m.categories || [],
1101
1231
  available: true
1102
1232
  }));
@@ -1184,6 +1314,58 @@ var LLMClient = class {
1184
1314
  const data = await this.requestWithPaymentRaw("/v1/search", body);
1185
1315
  return data;
1186
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
+ }
1187
1369
  /**
1188
1370
  * Get USDC balance on Base network.
1189
1371
  *
@@ -2197,6 +2379,55 @@ var SolanaLLMClient = class {
2197
2379
  async pmQuery(path5, query) {
2198
2380
  return this.requestWithPaymentRaw(`/v1/pm/${path5}`, query);
2199
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
+ }
2200
2431
  /** Get session spending. */
2201
2432
  getSpending() {
2202
2433
  return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
@@ -2578,7 +2809,7 @@ function readableFilename(endpoint, body) {
2578
2809
  ep = "image";
2579
2810
  }
2580
2811
  let label = body.query || body.username || body.handle || body.model || (typeof body.prompt === "string" ? body.prompt.slice(0, 40) : "") || "";
2581
- label = String(label).replace(/[^a-zA-Z0-9_\-]/g, "_").slice(0, 40).replace(/^_+|_+$/g, "");
2812
+ label = String(label).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40).replace(/^_+|_+$/g, "");
2582
2813
  return label ? `${ep}_${ts}_${label}.json` : `${ep}_${ts}.json`;
2583
2814
  }
2584
2815
  function saveReadable(endpoint, body, response, costUsd) {
@@ -2842,46 +3073,18 @@ var ChatCompletions = class {
2842
3073
  return this.transformResponse(response);
2843
3074
  }
2844
3075
  async createStream(params) {
2845
- const url = `${this.apiUrl}/v1/chat/completions`;
2846
- const body = {
2847
- model: params.model,
2848
- messages: params.messages,
2849
- max_tokens: params.max_tokens || 1024,
2850
- temperature: params.temperature,
2851
- top_p: params.top_p,
2852
- stream: true
2853
- };
2854
- if (params.tools) {
2855
- body.tools = params.tools;
2856
- }
2857
- if (params.tool_choice) {
2858
- body.tool_choice = params.tool_choice;
2859
- }
2860
- const controller = new AbortController();
2861
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2862
- try {
2863
- const response = await fetch(url, {
2864
- method: "POST",
2865
- headers: { "Content-Type": "application/json" },
2866
- body: JSON.stringify(body),
2867
- signal: controller.signal
2868
- });
2869
- if (response.status === 402) {
2870
- const paymentHeader = response.headers.get("payment-required");
2871
- if (!paymentHeader) {
2872
- throw new Error("402 response but no payment requirements found");
2873
- }
2874
- throw new Error(
2875
- "Streaming with automatic payment requires direct wallet access. Please use non-streaming mode or contact support for streaming setup."
2876
- );
2877
- }
2878
- if (!response.ok) {
2879
- 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
2880
3085
  }
2881
- return new StreamingResponse(response, params.model);
2882
- } finally {
2883
- clearTimeout(timeoutId);
2884
- }
3086
+ );
3087
+ return new StreamingResponse(response, params.model);
2885
3088
  }
2886
3089
  transformResponse(response) {
2887
3090
  return {
package/dist/index.d.cts CHANGED
@@ -296,6 +296,68 @@ interface SearchOptions {
296
296
  /** End date filter (YYYY-MM-DD) */
297
297
  toDate?: string;
298
298
  }
299
+ interface ExaSearchOptions {
300
+ /** Number of results to return (default: 10, max: 100) */
301
+ numResults?: number;
302
+ /** Restrict to a content category */
303
+ category?: "github" | "news" | "research paper" | "linkedin profile" | "personal site" | "tweet" | "financial report" | "pdf" | "company";
304
+ /** Only include pages published after this date (ISO 8601) */
305
+ startPublishedDate?: string;
306
+ /** Only include pages published before this date (ISO 8601) */
307
+ endPublishedDate?: string;
308
+ /** Only search within these domains */
309
+ includeDomains?: string[];
310
+ /** Exclude these domains from results */
311
+ excludeDomains?: string[];
312
+ }
313
+ interface ExaSearchItem {
314
+ id: string;
315
+ url: string;
316
+ title: string;
317
+ publishedDate?: string;
318
+ author?: string;
319
+ score?: number;
320
+ }
321
+ interface ExaSearchResponse {
322
+ requestId: string;
323
+ resolvedSearchType: string;
324
+ results: ExaSearchItem[];
325
+ searchTime: number;
326
+ costDollars: {
327
+ total: number;
328
+ };
329
+ }
330
+ interface ExaAnswerCitation {
331
+ id: string;
332
+ title: string;
333
+ url: string;
334
+ publishedDate?: string;
335
+ favicon?: string;
336
+ }
337
+ interface ExaAnswerResponse {
338
+ requestId: string;
339
+ answer: string;
340
+ citations: ExaAnswerCitation[];
341
+ }
342
+ interface ExaContentItem {
343
+ id: string;
344
+ url: string;
345
+ title: string;
346
+ text: string;
347
+ author?: string | null;
348
+ }
349
+ interface ExaContentsResponse {
350
+ results: ExaContentItem[];
351
+ costDollars: {
352
+ total: number;
353
+ };
354
+ }
355
+ interface ExaFindSimilarOptions {
356
+ /** Number of results to return (default: 10, max: 100) */
357
+ numResults?: number;
358
+ /** Exclude pages from the same domain as the reference URL */
359
+ excludeSourceDomain?: boolean;
360
+ }
299
361
  interface XUser {
300
362
  id: string;
301
363
  userName: string;
@@ -483,6 +545,8 @@ declare class LLMClient {
483
545
  private sessionCalls;
484
546
  private modelPricingCache;
485
547
  private modelPricingPromise;
548
+ private preAuthCache;
549
+ private static readonly PRE_AUTH_TTL_MS;
486
550
  /**
487
551
  * Initialize the BlockRun LLM client.
488
552
  *
@@ -559,6 +623,21 @@ declare class LLMClient {
559
623
  * Handle 402 response: parse requirements, sign payment, retry.
560
624
  */
561
625
  private handlePaymentAndRetry;
626
+ /**
627
+ * Sign a payment header and return the PAYMENT-SIGNATURE value.
628
+ * Extracted to share logic between streaming and non-streaming flows.
629
+ */
630
+ private signPayment;
631
+ /**
632
+ * Streaming chat completion with automatic x402 payment.
633
+ *
634
+ * Uses a pre-auth cache so repeat calls to the same model skip the 402
635
+ * round-trip (~200ms savings). Falls back to the normal 402 flow on cache
636
+ * miss or if the pre-signed payment is rejected.
637
+ *
638
+ * @returns Raw fetch Response with a streaming SSE body.
639
+ */
640
+ chatCompletionStream(model: string, messages: ChatMessage[], options?: ChatCompletionOptions): Promise<Response>;
562
641
  /**
563
642
  * Make a request with automatic x402 payment handling, returning raw JSON.
564
643
  * Used for non-ChatResponse endpoints (X/Twitter, search, image edit, etc.).
@@ -622,6 +701,36 @@ declare class LLMClient {
622
701
  * @returns SearchResult with summary and citations
623
702
  */
624
703
  search(query: string, options?: SearchOptions): Promise<SearchResult>;
704
+ /**
705
+ * Neural web search via Exa. Returns semantically relevant URLs and metadata.
706
+ * Understands meaning, not just keywords. $0.01/call.
707
+ *
708
+ * @param query - Natural language search query
709
+ * @param options - Optional filters (numResults, category, date range, domains)
710
+ */
711
+ exaSearch(query: string, options?: ExaSearchOptions): Promise<ExaSearchResponse>;
712
+ /**
713
+ * Ask a question and get a cited, synthesized answer grounded in real web sources.
714
+ * No hallucinations — every claim is backed by a citation. $0.01/call.
715
+ *
716
+ * @param query - The question to answer
717
+ */
718
+ exaAnswer(query: string): Promise<ExaAnswerResponse>;
719
+ /**
720
+ * Fetch full Markdown text content from a list of URLs. $0.002 per URL.
721
+ * Returns clean text ready to feed into an LLM context window.
722
+ *
723
+ * @param urls - Array of URLs to fetch (up to 100)
724
+ */
725
+ exaContents(urls: string[]): Promise<ExaContentsResponse>;
726
+ /**
727
+ * Find pages semantically similar to a given URL. $0.01/call.
728
+ * Useful for discovering competitors, alternatives, and related resources.
729
+ *
730
+ * @param url - Reference URL
731
+ * @param options - Optional filters (numResults, excludeSourceDomain)
732
+ */
733
+ exaFindSimilar(url: string, options?: ExaFindSimilarOptions): Promise<ExaSearchResponse>;
625
734
  /**
626
735
  * Get USDC balance on Base network.
627
736
  *
@@ -1204,6 +1313,44 @@ declare class SolanaLLMClient {
1204
1313
  xCompareAuthors(handle1: string, handle2: string): Promise<XCompareAuthorsResponse>;
1205
1314
  pm(path: string, params?: Record<string, string>): Promise<Record<string, unknown>>;
1206
1315
  pmQuery(path: string, query: Record<string, unknown>): Promise<Record<string, unknown>>;
1316
+ /**
1317
+ * Generic Exa endpoint proxy (POST, Solana payment). Powered by Exa.
1318
+ *
1319
+ * @param path - Exa endpoint: "search" | "find-similar" | "contents" | "answer"
1320
+ * @param body - Request body (see Exa API docs)
1321
+ *
1322
+ * @example
1323
+ * const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
1324
+ */
1325
+ exa(path: string, body: Record<string, unknown>): Promise<Record<string, unknown>>;
1326
+ /**
1327
+ * Neural and keyword web search via Exa (Solana payment, $0.01/request).
1328
+ *
1329
+ * @example
1330
+ * const results = await client.exaSearch("latest AI papers", { numResults: 5 });
1331
+ */
1332
+ exaSearch(query: string, options?: Record<string, unknown>): Promise<Record<string, unknown>>;
1333
+ /**
1334
+ * Find pages semantically similar to a given URL via Exa (Solana payment, $0.01/request).
1335
+ *
1336
+ * @example
1337
+ * const results = await client.exaFindSimilar("https://openai.com/research/gpt-4", { numResults: 5 });
1338
+ */
1339
+ exaFindSimilar(url: string, options?: Record<string, unknown>): Promise<Record<string, unknown>>;
1340
+ /**
1341
+ * Extract full text content from URLs via Exa (Solana payment, $0.002/URL).
1342
+ *
1343
+ * @example
1344
+ * const data = await client.exaContents(["https://arxiv.org/abs/2303.08774"]);
1345
+ */
1346
+ exaContents(urls: string[], options?: Record<string, unknown>): Promise<Record<string, unknown>>;
1347
+ /**
1348
+ * AI-generated answer grounded in live web search via Exa (Solana payment, $0.01/request).
1349
+ *
1350
+ * @example
1351
+ * const answer = await client.exaAnswer("What is the current state of AI safety research?");
1352
+ */
1353
+ exaAnswer(query: string, options?: Record<string, unknown>): Promise<Record<string, unknown>>;
1207
1354
  /** Get session spending. */
1208
1355
  getSpending(): Spending;
1209
1356
  /** True if using sol.blockrun.ai. */
package/dist/index.d.ts CHANGED
@@ -296,6 +296,68 @@ interface SearchOptions {
296
296
  /** End date filter (YYYY-MM-DD) */
297
297
  toDate?: string;
298
298
  }
299
+ interface ExaSearchOptions {
300
+ /** Number of results to return (default: 10, max: 100) */
301
+ numResults?: number;
302
+ /** Restrict to a content category */
303
+ category?: "github" | "news" | "research paper" | "linkedin profile" | "personal site" | "tweet" | "financial report" | "pdf" | "company";
304
+ /** Only include pages published after this date (ISO 8601) */
305
+ startPublishedDate?: string;
306
+ /** Only include pages published before this date (ISO 8601) */
307
+ endPublishedDate?: string;
308
+ /** Only search within these domains */
309
+ includeDomains?: string[];
310
+ /** Exclude these domains from results */
311
+ excludeDomains?: string[];
312
+ }
313
+ interface ExaSearchItem {
314
+ id: string;
315
+ url: string;
316
+ title: string;
317
+ publishedDate?: string;
318
+ author?: string;
319
+ score?: number;
320
+ }
321
+ interface ExaSearchResponse {
322
+ requestId: string;
323
+ resolvedSearchType: string;
324
+ results: ExaSearchItem[];
325
+ searchTime: number;
326
+ costDollars: {
327
+ total: number;
328
+ };
329
+ }
330
+ interface ExaAnswerCitation {
331
+ id: string;
332
+ title: string;
333
+ url: string;
334
+ publishedDate?: string;
335
+ favicon?: string;
336
+ }
337
+ interface ExaAnswerResponse {
338
+ requestId: string;
339
+ answer: string;
340
+ citations: ExaAnswerCitation[];
341
+ }
342
+ interface ExaContentItem {
343
+ id: string;
344
+ url: string;
345
+ title: string;
346
+ text: string;
347
+ author?: string | null;
348
+ }
349
+ interface ExaContentsResponse {
350
+ results: ExaContentItem[];
351
+ costDollars: {
352
+ total: number;
353
+ };
354
+ }
355
+ interface ExaFindSimilarOptions {
356
+ /** Number of results to return (default: 10, max: 100) */
357
+ numResults?: number;
358
+ /** Exclude pages from the same domain as the reference URL */
359
+ excludeSourceDomain?: boolean;
360
+ }
299
361
  interface XUser {
300
362
  id: string;
301
363
  userName: string;
@@ -483,6 +545,8 @@ declare class LLMClient {
483
545
  private sessionCalls;
484
546
  private modelPricingCache;
485
547
  private modelPricingPromise;
548
+ private preAuthCache;
549
+ private static readonly PRE_AUTH_TTL_MS;
486
550
  /**
487
551
  * Initialize the BlockRun LLM client.
488
552
  *
@@ -559,6 +623,21 @@ declare class LLMClient {
559
623
  * Handle 402 response: parse requirements, sign payment, retry.
560
624
  */
561
625
  private handlePaymentAndRetry;
626
+ /**
627
+ * Sign a payment header and return the PAYMENT-SIGNATURE value.
628
+ * Extracted to share logic between streaming and non-streaming flows.
629
+ */
630
+ private signPayment;
631
+ /**
632
+ * Streaming chat completion with automatic x402 payment.
633
+ *
634
+ * Uses a pre-auth cache so repeat calls to the same model skip the 402
635
+ * round-trip (~200ms savings). Falls back to the normal 402 flow on cache
636
+ * miss or if the pre-signed payment is rejected.
637
+ *
638
+ * @returns Raw fetch Response with a streaming SSE body.
639
+ */
640
+ chatCompletionStream(model: string, messages: ChatMessage[], options?: ChatCompletionOptions): Promise<Response>;
562
641
  /**
563
642
  * Make a request with automatic x402 payment handling, returning raw JSON.
564
643
  * Used for non-ChatResponse endpoints (X/Twitter, search, image edit, etc.).
@@ -622,6 +701,36 @@ declare class LLMClient {
622
701
  * @returns SearchResult with summary and citations
623
702
  */
624
703
  search(query: string, options?: SearchOptions): Promise<SearchResult>;
704
+ /**
705
+ * Neural web search via Exa. Returns semantically relevant URLs and metadata.
706
+ * Understands meaning, not just keywords. $0.01/call.
707
+ *
708
+ * @param query - Natural language search query
709
+ * @param options - Optional filters (numResults, category, date range, domains)
710
+ */
711
+ exaSearch(query: string, options?: ExaSearchOptions): Promise<ExaSearchResponse>;
712
+ /**
713
+ * Ask a question and get a cited, synthesized answer grounded in real web sources.
714
+ * No hallucinations — every claim is backed by a citation. $0.01/call.
715
+ *
716
+ * @param query - The question to answer
717
+ */
718
+ exaAnswer(query: string): Promise<ExaAnswerResponse>;
719
+ /**
720
+ * Fetch full Markdown text content from a list of URLs. $0.002 per URL.
721
+ * Returns clean text ready to feed into an LLM context window.
722
+ *
723
+ * @param urls - Array of URLs to fetch (up to 100)
724
+ */
725
+ exaContents(urls: string[]): Promise<ExaContentsResponse>;
726
+ /**
727
+ * Find pages semantically similar to a given URL. $0.01/call.
728
+ * Useful for discovering competitors, alternatives, and related resources.
729
+ *
730
+ * @param url - Reference URL
731
+ * @param options - Optional filters (numResults, excludeSourceDomain)
732
+ */
733
+ exaFindSimilar(url: string, options?: ExaFindSimilarOptions): Promise<ExaSearchResponse>;
625
734
  /**
626
735
  * Get USDC balance on Base network.
627
736
  *
@@ -1204,6 +1313,44 @@ declare class SolanaLLMClient {
1204
1313
  xCompareAuthors(handle1: string, handle2: string): Promise<XCompareAuthorsResponse>;
1205
1314
  pm(path: string, params?: Record<string, string>): Promise<Record<string, unknown>>;
1206
1315
  pmQuery(path: string, query: Record<string, unknown>): Promise<Record<string, unknown>>;
1316
+ /**
1317
+ * Generic Exa endpoint proxy (POST, Solana payment). Powered by Exa.
1318
+ *
1319
+ * @param path - Exa endpoint: "search" | "find-similar" | "contents" | "answer"
1320
+ * @param body - Request body (see Exa API docs)
1321
+ *
1322
+ * @example
1323
+ * const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
1324
+ */
1325
+ exa(path: string, body: Record<string, unknown>): Promise<Record<string, unknown>>;
1326
+ /**
1327
+ * Neural and keyword web search via Exa (Solana payment, $0.01/request).
1328
+ *
1329
+ * @example
1330
+ * const results = await client.exaSearch("latest AI papers", { numResults: 5 });
1331
+ */
1332
+ exaSearch(query: string, options?: Record<string, unknown>): Promise<Record<string, unknown>>;
1333
+ /**
1334
+ * Find pages semantically similar to a given URL via Exa (Solana payment, $0.01/request).
1335
+ *
1336
+ * @example
1337
+ * const results = await client.exaFindSimilar("https://openai.com/research/gpt-4", { numResults: 5 });
1338
+ */
1339
+ exaFindSimilar(url: string, options?: Record<string, unknown>): Promise<Record<string, unknown>>;
1340
+ /**
1341
+ * Extract full text content from URLs via Exa (Solana payment, $0.002/URL).
1342
+ *
1343
+ * @example
1344
+ * const data = await client.exaContents(["https://arxiv.org/abs/2303.08774"]);
1345
+ */
1346
+ exaContents(urls: string[], options?: Record<string, unknown>): Promise<Record<string, unknown>>;
1347
+ /**
1348
+ * AI-generated answer grounded in live web search via Exa (Solana payment, $0.01/request).
1349
+ *
1350
+ * @example
1351
+ * const answer = await client.exaAnswer("What is the current state of AI safety research?");
1352
+ */
1353
+ exaAnswer(query: string, options?: Record<string, unknown>): Promise<Record<string, unknown>>;
1207
1354
  /** Get session spending. */
1208
1355
  getSpending(): Spending;
1209
1356
  /** True if using sol.blockrun.ai. */
package/dist/index.js CHANGED
@@ -355,7 +355,7 @@ var DEFAULT_MAX_TOKENS = 1024;
355
355
  var DEFAULT_TIMEOUT = 6e4;
356
356
  var SDK_VERSION = "1.5.0";
357
357
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
358
- var LLMClient = class {
358
+ var LLMClient = class _LLMClient {
359
359
  static DEFAULT_API_URL = DEFAULT_API_URL;
360
360
  static TESTNET_API_URL = TESTNET_API_URL;
361
361
  account;
@@ -366,6 +366,11 @@ var LLMClient = class {
366
366
  sessionCalls = 0;
367
367
  modelPricingCache = null;
368
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;
369
374
  /**
370
375
  * Initialize the BlockRun LLM client.
371
376
  *
@@ -677,6 +682,131 @@ var LLMClient = class {
677
682
  this.sessionTotalUsd += costUsd;
678
683
  return retryResponse.json();
679
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
+ }
680
810
  /**
681
811
  * Make a request with automatic x402 payment handling, returning raw JSON.
682
812
  * Used for non-ChatResponse endpoints (X/Twitter, search, image edit, etc.).
@@ -1006,12 +1136,12 @@ var LLMClient = class {
1006
1136
  return (data.data || []).map((m) => ({
1007
1137
  id: m.id,
1008
1138
  name: m.name || m.id,
1009
- provider: m.owned_by || "",
1139
+ provider: m.provider || m.owned_by || "",
1010
1140
  description: m.description || "",
1011
- inputPrice: m.pricing?.input ?? m.pricing?.flat ?? 0,
1012
- outputPrice: m.pricing?.output ?? 0,
1013
- contextWindow: m.context_window || 0,
1014
- 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,
1015
1145
  categories: m.categories || [],
1016
1146
  available: true
1017
1147
  }));
@@ -1099,6 +1229,58 @@ var LLMClient = class {
1099
1229
  const data = await this.requestWithPaymentRaw("/v1/search", body);
1100
1230
  return data;
1101
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
+ }
1102
1284
  /**
1103
1285
  * Get USDC balance on Base network.
1104
1286
  *
@@ -2112,6 +2294,55 @@ var SolanaLLMClient = class {
2112
2294
  async pmQuery(path5, query) {
2113
2295
  return this.requestWithPaymentRaw(`/v1/pm/${path5}`, query);
2114
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
+ }
2115
2346
  /** Get session spending. */
2116
2347
  getSpending() {
2117
2348
  return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
@@ -2493,7 +2724,7 @@ function readableFilename(endpoint, body) {
2493
2724
  ep = "image";
2494
2725
  }
2495
2726
  let label = body.query || body.username || body.handle || body.model || (typeof body.prompt === "string" ? body.prompt.slice(0, 40) : "") || "";
2496
- label = String(label).replace(/[^a-zA-Z0-9_\-]/g, "_").slice(0, 40).replace(/^_+|_+$/g, "");
2727
+ label = String(label).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40).replace(/^_+|_+$/g, "");
2497
2728
  return label ? `${ep}_${ts}_${label}.json` : `${ep}_${ts}.json`;
2498
2729
  }
2499
2730
  function saveReadable(endpoint, body, response, costUsd) {
@@ -2757,46 +2988,18 @@ var ChatCompletions = class {
2757
2988
  return this.transformResponse(response);
2758
2989
  }
2759
2990
  async createStream(params) {
2760
- const url = `${this.apiUrl}/v1/chat/completions`;
2761
- const body = {
2762
- model: params.model,
2763
- messages: params.messages,
2764
- max_tokens: params.max_tokens || 1024,
2765
- temperature: params.temperature,
2766
- top_p: params.top_p,
2767
- stream: true
2768
- };
2769
- if (params.tools) {
2770
- body.tools = params.tools;
2771
- }
2772
- if (params.tool_choice) {
2773
- body.tool_choice = params.tool_choice;
2774
- }
2775
- const controller = new AbortController();
2776
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2777
- try {
2778
- const response = await fetch(url, {
2779
- method: "POST",
2780
- headers: { "Content-Type": "application/json" },
2781
- body: JSON.stringify(body),
2782
- signal: controller.signal
2783
- });
2784
- if (response.status === 402) {
2785
- const paymentHeader = response.headers.get("payment-required");
2786
- if (!paymentHeader) {
2787
- throw new Error("402 response but no payment requirements found");
2788
- }
2789
- throw new Error(
2790
- "Streaming with automatic payment requires direct wallet access. Please use non-streaming mode or contact support for streaming setup."
2791
- );
2792
- }
2793
- if (!response.ok) {
2794
- 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
2795
3000
  }
2796
- return new StreamingResponse(response, params.model);
2797
- } finally {
2798
- clearTimeout(timeoutId);
2799
- }
3001
+ );
3002
+ return new StreamingResponse(response, params.model);
2800
3003
  }
2801
3004
  transformResponse(response) {
2802
3005
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/llm",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "type": "module",
5
5
  "description": "BlockRun LLM Gateway SDK - Pay-per-request AI via x402 on Base and Solana",
6
6
  "main": "dist/index.cjs",