@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 +40 -0
- package/dist/index.cjs +249 -46
- package/dist/index.d.cts +147 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +249 -46
- package/package.json +1 -1
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.
|
|
1097
|
-
outputPrice: m.pricing?.output ?? 0,
|
|
1098
|
-
contextWindow: m.context_window
|
|
1099
|
-
maxOutput: m.max_output
|
|
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_
|
|
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
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
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
|
-
|
|
2882
|
-
|
|
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.
|
|
1012
|
-
outputPrice: m.pricing?.output ?? 0,
|
|
1013
|
-
contextWindow: m.context_window
|
|
1014
|
-
maxOutput: m.max_output
|
|
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_
|
|
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
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
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
|
-
|
|
2797
|
-
|
|
2798
|
-
clearTimeout(timeoutId);
|
|
2799
|
-
}
|
|
3001
|
+
);
|
|
3002
|
+
return new StreamingResponse(response, params.model);
|
|
2800
3003
|
}
|
|
2801
3004
|
transformResponse(response) {
|
|
2802
3005
|
return {
|