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