@blockrun/llm 3.24.8 → 3.24.10
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 +5 -2
- package/dist/index.cjs +298 -100
- package/dist/index.d.cts +77 -12
- package/dist/index.d.ts +77 -12
- package/dist/index.js +298 -100
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -554,13 +554,44 @@ function readSettlement(response, quotedUsd, scheme) {
|
|
|
554
554
|
};
|
|
555
555
|
return { costUsd, settlement };
|
|
556
556
|
}
|
|
557
|
+
function settlementSkipReason(response) {
|
|
558
|
+
const headers = response.headers;
|
|
559
|
+
if (typeof headers?.get !== "function") return void 0;
|
|
560
|
+
const reason = [headers.get("X-Settlement-Skipped"), headers.get("X-Payment-Waived")].map((value) => value?.trim()).find((value) => !!value && !NOT_A_SKIP.test(value));
|
|
561
|
+
if (reason === void 0) return void 0;
|
|
562
|
+
const evidence = settlementEvidence(response);
|
|
563
|
+
if (evidence !== void 0) {
|
|
564
|
+
console.error(
|
|
565
|
+
`[@blockrun/llm] settlement: the gateway named a skipped settlement (${JSON.stringify(reason)}) on a response that also carries ${evidence}; booked as charged`
|
|
566
|
+
);
|
|
567
|
+
return void 0;
|
|
568
|
+
}
|
|
569
|
+
return reason;
|
|
570
|
+
}
|
|
571
|
+
var NOT_A_SKIP = /^(false|0|no|none)$/i;
|
|
572
|
+
function settlementEvidence(response) {
|
|
573
|
+
const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
|
|
574
|
+
const receipt = header ? decode(header) : void 0;
|
|
575
|
+
if (receipt && receipt.success !== false) {
|
|
576
|
+
if (typeof receipt.transaction === "string" && receipt.transaction) return "a settlement receipt with a transaction";
|
|
577
|
+
const extra = receipt.extra;
|
|
578
|
+
const charged = receiptChargedUsd({
|
|
579
|
+
amount: receipt.amount,
|
|
580
|
+
extra: extra && typeof extra === "object" ? extra : void 0
|
|
581
|
+
});
|
|
582
|
+
if (charged !== void 0 && charged > 0) return "a settlement receipt with a charge";
|
|
583
|
+
}
|
|
584
|
+
if (response.headers.get("X-Payment-Receipt")?.trim()) return "an X-Payment-Receipt transaction";
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
557
587
|
function settlementSkipped(response) {
|
|
558
|
-
return response
|
|
588
|
+
return settlementSkipReason(response) !== void 0;
|
|
559
589
|
}
|
|
560
590
|
function withCost(body, response, bookedUsd, scheme) {
|
|
561
|
-
|
|
591
|
+
const skipped = settlementSkipReason(response);
|
|
592
|
+
if (skipped !== void 0) {
|
|
562
593
|
body.costUsd = 0;
|
|
563
|
-
body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped
|
|
594
|
+
body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped };
|
|
564
595
|
return body;
|
|
565
596
|
}
|
|
566
597
|
body.costUsd = bookedUsd;
|
|
@@ -5089,7 +5120,7 @@ function getCostSummary() {
|
|
|
5089
5120
|
}
|
|
5090
5121
|
|
|
5091
5122
|
// src/version.ts
|
|
5092
|
-
var SDK_VERSION = "3.24.
|
|
5123
|
+
var SDK_VERSION = "3.24.10";
|
|
5093
5124
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
5094
5125
|
|
|
5095
5126
|
// src/client.ts
|
|
@@ -5685,9 +5716,9 @@ var LLMClient = class _LLMClient {
|
|
|
5685
5716
|
return this.bookPaid(url, body, details, retryResponse);
|
|
5686
5717
|
}
|
|
5687
5718
|
/**
|
|
5688
|
-
* Book a served, paid chat call and attach its cost. A response
|
|
5689
|
-
*
|
|
5690
|
-
*
|
|
5719
|
+
* Book a served, paid chat call and attach its cost. A response whose
|
|
5720
|
+
* settlement the gateway skipped ({@link settlementSkipped}: e.g.
|
|
5721
|
+
* `X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
|
|
5691
5722
|
* not added to `getSpending()` or the cost log, and reports `costUsd: 0`.
|
|
5692
5723
|
*/
|
|
5693
5724
|
async bookPaid(url, body, details, response) {
|
|
@@ -5699,6 +5730,21 @@ var LLMClient = class _LLMClient {
|
|
|
5699
5730
|
}
|
|
5700
5731
|
return withCost(await this.parseChatResponse(response), response, costUsd, "exact");
|
|
5701
5732
|
}
|
|
5733
|
+
/**
|
|
5734
|
+
* Book a served, paid raw (non-chat) call: the signed amount goes to
|
|
5735
|
+
* `getSpending()` and the cost log, unless the gateway skipped settlement
|
|
5736
|
+
* ({@link settlementSkipped}: e.g. `X-Settlement-Skipped: empty-result` on an
|
|
5737
|
+
* empty prediction-market search, `unexecuted` on a JSON-RPC call that never
|
|
5738
|
+
* ran, or `X-Payment-Waived`), in which case nothing moved and nothing is
|
|
5739
|
+
* booked. `X-Payment-Settled: false` alone is still booked: it is owed.
|
|
5740
|
+
*/
|
|
5741
|
+
bookPaidRaw(url, details, response, body) {
|
|
5742
|
+
if (settlementSkipped(response)) return;
|
|
5743
|
+
const costUsd = parseFloat(details.amount) / 1e6;
|
|
5744
|
+
this.sessionCalls += 1;
|
|
5745
|
+
this.sessionTotalUsd += costUsd;
|
|
5746
|
+
this.recordCost(url, costUsd, body ? { body, network: details.network } : { network: details.network });
|
|
5747
|
+
}
|
|
5702
5748
|
/**
|
|
5703
5749
|
* Sign a payment header and return the PAYMENT-SIGNATURE value.
|
|
5704
5750
|
* Extracted to share logic between streaming and non-streaming flows.
|
|
@@ -5799,8 +5845,10 @@ var LLMClient = class _LLMClient {
|
|
|
5799
5845
|
throw withDisposition(err, "paid-or-in-doubt");
|
|
5800
5846
|
}
|
|
5801
5847
|
if (preAuthResp.ok) {
|
|
5802
|
-
|
|
5803
|
-
|
|
5848
|
+
if (!settlementSkipped(preAuthResp)) {
|
|
5849
|
+
this.sessionCalls += 1;
|
|
5850
|
+
this.sessionTotalUsd += signed.costUsd;
|
|
5851
|
+
}
|
|
5804
5852
|
return preAuthResp;
|
|
5805
5853
|
}
|
|
5806
5854
|
this.preAuthCache.delete(cacheKey2);
|
|
@@ -5865,8 +5913,10 @@ var LLMClient = class _LLMClient {
|
|
|
5865
5913
|
}
|
|
5866
5914
|
throw new APIError(`API error after payment: ${streamResp.status}`, streamResp.status, sanitizeErrorResponse(errorBody));
|
|
5867
5915
|
}
|
|
5868
|
-
|
|
5869
|
-
|
|
5916
|
+
if (!settlementSkipped(streamResp)) {
|
|
5917
|
+
this.sessionCalls += 1;
|
|
5918
|
+
this.sessionTotalUsd += costUsd;
|
|
5919
|
+
}
|
|
5870
5920
|
return streamResp;
|
|
5871
5921
|
}
|
|
5872
5922
|
/**
|
|
@@ -5990,10 +6040,7 @@ var LLMClient = class _LLMClient {
|
|
|
5990
6040
|
}
|
|
5991
6041
|
throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
|
|
5992
6042
|
}
|
|
5993
|
-
|
|
5994
|
-
this.sessionCalls += 1;
|
|
5995
|
-
this.sessionTotalUsd += costUsd2;
|
|
5996
|
-
this.recordCost(url, costUsd2, { body, network: details.network });
|
|
6043
|
+
this.bookPaidRaw(url, details, retryResp2, body);
|
|
5997
6044
|
return retryResp2.json();
|
|
5998
6045
|
}
|
|
5999
6046
|
}
|
|
@@ -6013,10 +6060,7 @@ var LLMClient = class _LLMClient {
|
|
|
6013
6060
|
sanitizeErrorResponse(errorBody)
|
|
6014
6061
|
);
|
|
6015
6062
|
}
|
|
6016
|
-
|
|
6017
|
-
this.sessionCalls += 1;
|
|
6018
|
-
this.sessionTotalUsd += costUsd;
|
|
6019
|
-
this.recordCost(url, costUsd, { body, network: details.network });
|
|
6063
|
+
this.bookPaidRaw(url, details, retryResponse, body);
|
|
6020
6064
|
return retryResponse.json();
|
|
6021
6065
|
}
|
|
6022
6066
|
/**
|
|
@@ -6136,10 +6180,7 @@ var LLMClient = class _LLMClient {
|
|
|
6136
6180
|
}
|
|
6137
6181
|
throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
|
|
6138
6182
|
}
|
|
6139
|
-
|
|
6140
|
-
this.sessionCalls += 1;
|
|
6141
|
-
this.sessionTotalUsd += costUsd2;
|
|
6142
|
-
this.recordCost(url, costUsd2, { network: details.network });
|
|
6183
|
+
this.bookPaidRaw(url, details, retryResp2);
|
|
6143
6184
|
return retryResp2.json();
|
|
6144
6185
|
}
|
|
6145
6186
|
}
|
|
@@ -6159,10 +6200,7 @@ var LLMClient = class _LLMClient {
|
|
|
6159
6200
|
sanitizeErrorResponse(errorBody)
|
|
6160
6201
|
);
|
|
6161
6202
|
}
|
|
6162
|
-
|
|
6163
|
-
this.sessionCalls += 1;
|
|
6164
|
-
this.sessionTotalUsd += costUsd;
|
|
6165
|
-
this.recordCost(url, costUsd, { network: details.network });
|
|
6203
|
+
this.bookPaidRaw(url, details, retryResponse);
|
|
6166
6204
|
return retryResponse.json();
|
|
6167
6205
|
}
|
|
6168
6206
|
/**
|
|
@@ -7175,8 +7213,10 @@ var MusicClient = class {
|
|
|
7175
7213
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
7176
7214
|
}
|
|
7177
7215
|
const data = await retryResponse.json();
|
|
7178
|
-
|
|
7179
|
-
|
|
7216
|
+
if (!settlementSkipped(retryResponse)) {
|
|
7217
|
+
this.sessionCalls++;
|
|
7218
|
+
this.sessionTotalUsd += 0.1575;
|
|
7219
|
+
}
|
|
7180
7220
|
const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
|
|
7181
7221
|
if (txHash) data.txHash = txHash;
|
|
7182
7222
|
return data;
|
|
@@ -7388,9 +7428,11 @@ var SpeechClient = class {
|
|
|
7388
7428
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
7389
7429
|
}
|
|
7390
7430
|
const data = await retryResponse.json();
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7431
|
+
if (!settlementSkipped(retryResponse)) {
|
|
7432
|
+
this.sessionCalls++;
|
|
7433
|
+
const paidUsd = Number(details.amount) / 1e6;
|
|
7434
|
+
if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
|
|
7435
|
+
}
|
|
7394
7436
|
const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
|
|
7395
7437
|
if (txHash) data.txHash = txHash;
|
|
7396
7438
|
return data;
|
|
@@ -7687,8 +7729,10 @@ var VideoClient = class {
|
|
|
7687
7729
|
}
|
|
7688
7730
|
if (lastStatus === "completed") {
|
|
7689
7731
|
const data = pollData;
|
|
7690
|
-
|
|
7691
|
-
|
|
7732
|
+
if (!settlementSkipped(pollResp)) {
|
|
7733
|
+
this.sessionCalls++;
|
|
7734
|
+
this.sessionTotalUsd += Number(details.amount) / 1e6;
|
|
7735
|
+
}
|
|
7692
7736
|
const txHash = pollResp.headers.get("x-payment-receipt") || pollResp.headers.get("X-Payment-Receipt");
|
|
7693
7737
|
if (txHash) data.txHash = txHash;
|
|
7694
7738
|
return data;
|
|
@@ -7983,8 +8027,10 @@ var VoiceClient = class {
|
|
|
7983
8027
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
7984
8028
|
}
|
|
7985
8029
|
const data = await retryResponse.json();
|
|
7986
|
-
|
|
7987
|
-
|
|
8030
|
+
if (!settlementSkipped(retryResponse)) {
|
|
8031
|
+
this.sessionCalls++;
|
|
8032
|
+
this.sessionTotalUsd += CALL_PRICE_USD;
|
|
8033
|
+
}
|
|
7988
8034
|
const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
|
|
7989
8035
|
if (txHash) data.txHash = txHash;
|
|
7990
8036
|
return data;
|
|
@@ -8204,9 +8250,10 @@ var PhoneClient = class {
|
|
|
8204
8250
|
throw new PaymentError("Payment was rejected. Check your wallet balance.");
|
|
8205
8251
|
}
|
|
8206
8252
|
const data = await this.unwrap(retry, true);
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8253
|
+
if (!settlementSkipped(retry)) {
|
|
8254
|
+
this.sessionCalls += 1;
|
|
8255
|
+
this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
|
|
8256
|
+
}
|
|
8210
8257
|
const txHash = retry.headers.get("x-payment-receipt") || retry.headers.get("X-Payment-Receipt");
|
|
8211
8258
|
if (txHash && data && typeof data === "object" && !("txHash" in data)) {
|
|
8212
8259
|
data.txHash = txHash;
|
|
@@ -8371,9 +8418,10 @@ var PortraitClient = class {
|
|
|
8371
8418
|
throw new PaymentError("Payment was rejected. Check your wallet balance.");
|
|
8372
8419
|
}
|
|
8373
8420
|
const data = await this.unwrap(retry, true);
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8421
|
+
if (!settlementSkipped(retry)) {
|
|
8422
|
+
this.sessionCalls += 1;
|
|
8423
|
+
this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
|
|
8424
|
+
}
|
|
8377
8425
|
if (data && typeof data === "object" && !("txHash" in data)) {
|
|
8378
8426
|
const settlement = data.settlement;
|
|
8379
8427
|
if (settlement?.tx_hash) {
|
|
@@ -9190,9 +9238,11 @@ var RpcClient = class {
|
|
|
9190
9238
|
}
|
|
9191
9239
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
9192
9240
|
}
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9241
|
+
if (!settlementSkipped(retryResponse)) {
|
|
9242
|
+
this.sessionCalls++;
|
|
9243
|
+
const paidUsd = Number(details.amount) / 1e6;
|
|
9244
|
+
if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
|
|
9245
|
+
}
|
|
9196
9246
|
return { data: await retryResponse.json(), headers: retryResponse.headers };
|
|
9197
9247
|
}
|
|
9198
9248
|
async fetchWithTimeout(url, options) {
|
|
@@ -9339,7 +9389,7 @@ var BlockrunClient = class {
|
|
|
9339
9389
|
if (resp402.status !== 402) {
|
|
9340
9390
|
await this.throwApiError(resp402, `poll submit failed (${submitUrl})`);
|
|
9341
9391
|
}
|
|
9342
|
-
const paymentPayload = await this.signFrom402(resp402, submitUrl, {
|
|
9392
|
+
const { paymentPayload, costUsd } = await this.signFrom402(resp402, submitUrl, {
|
|
9343
9393
|
description: "BlockRun async job",
|
|
9344
9394
|
maxTimeoutSeconds: MAX_SIGNED_AUTH_SECONDS
|
|
9345
9395
|
});
|
|
@@ -9358,7 +9408,7 @@ var BlockrunClient = class {
|
|
|
9358
9408
|
await this.throwApiError(submitResp, `poll submit failed (${submitUrl})`);
|
|
9359
9409
|
}
|
|
9360
9410
|
if (submitResp.status === 200) {
|
|
9361
|
-
this.recordSpending();
|
|
9411
|
+
this.recordSpending(submitResp, costUsd);
|
|
9362
9412
|
return submitResp.json();
|
|
9363
9413
|
}
|
|
9364
9414
|
const submitData = await submitResp.json();
|
|
@@ -9395,7 +9445,7 @@ var BlockrunClient = class {
|
|
|
9395
9445
|
);
|
|
9396
9446
|
}
|
|
9397
9447
|
if (pollResp.status === 200 && lastStatus === "completed") {
|
|
9398
|
-
this.recordSpending();
|
|
9448
|
+
this.recordSpending(pollResp, costUsd);
|
|
9399
9449
|
return pollData;
|
|
9400
9450
|
}
|
|
9401
9451
|
if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
|
|
@@ -9436,7 +9486,7 @@ var BlockrunClient = class {
|
|
|
9436
9486
|
if (resp402.status === 200) {
|
|
9437
9487
|
streamResp = resp402;
|
|
9438
9488
|
} else if (resp402.status === 402) {
|
|
9439
|
-
const paymentPayload = await this.signFrom402(resp402, url, {
|
|
9489
|
+
const { paymentPayload, costUsd } = await this.signFrom402(resp402, url, {
|
|
9440
9490
|
description: "BlockRun stream"
|
|
9441
9491
|
});
|
|
9442
9492
|
streamResp = await this.fetchWithTimeout(url, {
|
|
@@ -9455,7 +9505,7 @@ var BlockrunClient = class {
|
|
|
9455
9505
|
if (!streamResp.ok) {
|
|
9456
9506
|
await this.throwApiError(streamResp, `stream failed after payment (${url})`);
|
|
9457
9507
|
}
|
|
9458
|
-
this.recordSpending();
|
|
9508
|
+
this.recordSpending(streamResp, costUsd);
|
|
9459
9509
|
} else {
|
|
9460
9510
|
await this.throwApiError(resp402, `stream failed (${url})`);
|
|
9461
9511
|
return;
|
|
@@ -9511,7 +9561,7 @@ var BlockrunClient = class {
|
|
|
9511
9561
|
return response.json();
|
|
9512
9562
|
}
|
|
9513
9563
|
async handlePaymentAndRetry(url, method, body, response) {
|
|
9514
|
-
const paymentPayload = await this.signFrom402(response, url, {
|
|
9564
|
+
const { paymentPayload, costUsd } = await this.signFrom402(response, url, {
|
|
9515
9565
|
description: "BlockRun"
|
|
9516
9566
|
});
|
|
9517
9567
|
const retryInit = {
|
|
@@ -9534,13 +9584,14 @@ var BlockrunClient = class {
|
|
|
9534
9584
|
if (!retry.ok) {
|
|
9535
9585
|
await this.throwApiError(retry, `${method} ${url} failed after payment`);
|
|
9536
9586
|
}
|
|
9537
|
-
this.recordSpending();
|
|
9587
|
+
this.recordSpending(retry, costUsd);
|
|
9538
9588
|
return retry.json();
|
|
9539
9589
|
}
|
|
9540
9590
|
/**
|
|
9541
9591
|
* Read a 402 response's payment requirements (header or body), then sign and
|
|
9542
|
-
* return the base64 PAYMENT-SIGNATURE payload
|
|
9543
|
-
*
|
|
9592
|
+
* return the base64 PAYMENT-SIGNATURE payload with what it pays, in USD.
|
|
9593
|
+
* The cost stays with the call that signed it (booked by `recordSpending`):
|
|
9594
|
+
* concurrent calls on one client never share it.
|
|
9544
9595
|
*/
|
|
9545
9596
|
async signFrom402(response, url, opts) {
|
|
9546
9597
|
let paymentHeader = response.headers.get("payment-required");
|
|
@@ -9558,8 +9609,8 @@ var BlockrunClient = class {
|
|
|
9558
9609
|
}
|
|
9559
9610
|
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
9560
9611
|
const details = extractPaymentDetails(paymentRequired);
|
|
9561
|
-
|
|
9562
|
-
|
|
9612
|
+
const costUsd = parseFloat(details.amount) / 1e6;
|
|
9613
|
+
const paymentPayload = await createPaymentPayload(
|
|
9563
9614
|
this.privateKey,
|
|
9564
9615
|
this.account.address,
|
|
9565
9616
|
details.recipient,
|
|
@@ -9576,14 +9627,20 @@ var BlockrunClient = class {
|
|
|
9576
9627
|
asset: details.asset
|
|
9577
9628
|
}
|
|
9578
9629
|
);
|
|
9630
|
+
return { paymentPayload, costUsd };
|
|
9579
9631
|
}
|
|
9580
|
-
/**
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9632
|
+
/**
|
|
9633
|
+
* Book `costUsd`, what the call that `response` answered signed. A response
|
|
9634
|
+
* whose settlement the gateway skipped ({@link settlementSkipped}:
|
|
9635
|
+
* `X-Settlement-Skipped: <reason>` or `X-Payment-Waived`) moved nothing, so
|
|
9636
|
+
* nothing is booked. `X-Payment-Settled: false` alone is still booked: the
|
|
9637
|
+
* payment is owed.
|
|
9638
|
+
*/
|
|
9639
|
+
recordSpending(response, costUsd) {
|
|
9640
|
+
if (settlementSkipped(response)) return;
|
|
9641
|
+
if (costUsd > 0) {
|
|
9584
9642
|
this.sessionCalls += 1;
|
|
9585
|
-
this.sessionTotalUsd +=
|
|
9586
|
-
this.pendingCostUsd = 0;
|
|
9643
|
+
this.sessionTotalUsd += costUsd;
|
|
9587
9644
|
}
|
|
9588
9645
|
}
|
|
9589
9646
|
async throwApiError(resp, prefix) {
|
|
@@ -10845,6 +10902,38 @@ function withoutClientSignedBatch(paymentRequired) {
|
|
|
10845
10902
|
const accepts = paymentRequired.accepts ?? [];
|
|
10846
10903
|
return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
|
|
10847
10904
|
}
|
|
10905
|
+
var DEPOSIT_BLOCKHASH_TTL_MS = 1e4;
|
|
10906
|
+
var MAX_DEPOSIT_BLOCKHASH_ENDPOINTS = 8;
|
|
10907
|
+
var depositBlockhashes = /* @__PURE__ */ new Map();
|
|
10908
|
+
async function finalizedBlockhash(rpcUrl, rpcHeaders) {
|
|
10909
|
+
const cached = depositBlockhashes.get(rpcUrl);
|
|
10910
|
+
if (cached && Date.now() - cached.fetchedAt < DEPOSIT_BLOCKHASH_TTL_MS) return cached;
|
|
10911
|
+
const result = await rpcRequest(rpcUrl, "getLatestBlockhash", [{ commitment: "finalized" }], rpcHeaders);
|
|
10912
|
+
const blockhash = result.value?.blockhash;
|
|
10913
|
+
if (typeof blockhash !== "string" || !blockhash) throw new Error("getLatestBlockhash returned no blockhash");
|
|
10914
|
+
const entry = {
|
|
10915
|
+
blockhash,
|
|
10916
|
+
lastValidBlockHeight: rpcInteger(result.value?.lastValidBlockHeight, "getLatestBlockhash lastValidBlockHeight"),
|
|
10917
|
+
fetchedAt: Date.now()
|
|
10918
|
+
};
|
|
10919
|
+
depositBlockhashes.delete(rpcUrl);
|
|
10920
|
+
depositBlockhashes.set(rpcUrl, entry);
|
|
10921
|
+
while (depositBlockhashes.size > MAX_DEPOSIT_BLOCKHASH_ENDPOINTS) {
|
|
10922
|
+
depositBlockhashes.delete(depositBlockhashes.keys().next().value);
|
|
10923
|
+
}
|
|
10924
|
+
return entry;
|
|
10925
|
+
}
|
|
10926
|
+
function withDepositBlockhash(paymentRequired, blockhash) {
|
|
10927
|
+
const accepts = paymentRequired.accepts ?? [];
|
|
10928
|
+
const lacking = (accept) => accept.scheme === BATCH_SCHEME && !accept.extra?.recentBlockhash;
|
|
10929
|
+
if (!accepts.some(lacking)) return paymentRequired;
|
|
10930
|
+
return {
|
|
10931
|
+
...paymentRequired,
|
|
10932
|
+
accepts: accepts.map(
|
|
10933
|
+
(accept) => lacking(accept) ? { ...accept, extra: { ...accept.extra, recentBlockhash: blockhash.blockhash, lastValidBlockHeight: blockhash.lastValidBlockHeight } } : accept
|
|
10934
|
+
)
|
|
10935
|
+
};
|
|
10936
|
+
}
|
|
10848
10937
|
var PAYMENT_IDENTIFIER = "payment-identifier";
|
|
10849
10938
|
function withPaymentIdentifier(paymentRequired) {
|
|
10850
10939
|
const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
|
|
@@ -11802,6 +11891,8 @@ var SolanaBatchPayer = class {
|
|
|
11802
11891
|
wallet.refusedFor = outcome.errorReason;
|
|
11803
11892
|
}
|
|
11804
11893
|
}
|
|
11894
|
+
} else if (built.key && outcome.reason === "payment_required" && outcome.refusedForMs === void 0) {
|
|
11895
|
+
await this.recheckRefusedChannel(wallet, built.key, built.reclaimAnchor);
|
|
11805
11896
|
}
|
|
11806
11897
|
sent = void 0;
|
|
11807
11898
|
release();
|
|
@@ -12069,6 +12160,13 @@ var SolanaBatchPayer = class {
|
|
|
12069
12160
|
}
|
|
12070
12161
|
/** Build a new batch payment for a 402: the payload, its headers and its kind. */
|
|
12071
12162
|
async buildPayment(wallet, paymentRequired) {
|
|
12163
|
+
const acceptKey = channelKeyOf(trustedBatchAccept(paymentRequired, this.init.options.operators));
|
|
12164
|
+
let reclaimAnchor;
|
|
12165
|
+
try {
|
|
12166
|
+
const stored = acceptKey ? await wallet.book.base.get(acceptKey) : void 0;
|
|
12167
|
+
reclaimAnchor = stored ? savedReclaimAnchor(stored) : void 0;
|
|
12168
|
+
} catch {
|
|
12169
|
+
}
|
|
12072
12170
|
const { http, payload } = await this.createPayload(wallet, paymentRequired);
|
|
12073
12171
|
const scope = payScope.getStore();
|
|
12074
12172
|
if (scope?.afterError) {
|
|
@@ -12090,7 +12188,16 @@ var SolanaBatchPayer = class {
|
|
|
12090
12188
|
const record = known?.record;
|
|
12091
12189
|
kind = record && (record.pending === void 0 || record.hasConfirmedState) ? "top-up" : "open";
|
|
12092
12190
|
}
|
|
12093
|
-
return {
|
|
12191
|
+
return {
|
|
12192
|
+
http,
|
|
12193
|
+
payload,
|
|
12194
|
+
headers,
|
|
12195
|
+
kind,
|
|
12196
|
+
key: known?.key,
|
|
12197
|
+
...reclaimAnchor !== void 0 && known?.key === acceptKey ? { reclaimAnchor } : {},
|
|
12198
|
+
sentAt: Date.now(),
|
|
12199
|
+
sends: 0
|
|
12200
|
+
};
|
|
12094
12201
|
}
|
|
12095
12202
|
/**
|
|
12096
12203
|
* Write a deposit's intent to the wallet's journal before it is sent
|
|
@@ -12316,7 +12423,8 @@ var SolanaBatchPayer = class {
|
|
|
12316
12423
|
const http = (await this.build(wallet)).http;
|
|
12317
12424
|
try {
|
|
12318
12425
|
await this.prepareTopUp(wallet, paymentRequired);
|
|
12319
|
-
|
|
12426
|
+
const required = await this.withDepositBlockhash(wallet, withoutClientSignedBatch(paymentRequired));
|
|
12427
|
+
return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(required)) };
|
|
12320
12428
|
} catch (err) {
|
|
12321
12429
|
if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
|
|
12322
12430
|
} finally {
|
|
@@ -12324,6 +12432,112 @@ var SolanaBatchPayer = class {
|
|
|
12324
12432
|
}
|
|
12325
12433
|
}
|
|
12326
12434
|
}
|
|
12435
|
+
/**
|
|
12436
|
+
* The 402 to build this payment from: with a finalized blockhash
|
|
12437
|
+
* ({@link withDepositBlockhash}) when the payment will likely carry a
|
|
12438
|
+
* deposit (no usable record for the trusted accept's channel, so an open,
|
|
12439
|
+
* or a top-up {@link prepareTopUp} just sized). An authorization signs no
|
|
12440
|
+
* transaction and costs no RPC read. Best effort: a failed read leaves the
|
|
12441
|
+
* 402 as it is, and `@x402/svm` fetches its own blockhash.
|
|
12442
|
+
*/
|
|
12443
|
+
async withDepositBlockhash(wallet, paymentRequired) {
|
|
12444
|
+
const accept = trustedBatchAccept(paymentRequired, this.init.options.operators);
|
|
12445
|
+
const key = accept ? channelKeyOf(accept) : void 0;
|
|
12446
|
+
if (!accept || accept.extra?.recentBlockhash) return paymentRequired;
|
|
12447
|
+
let deposit = wallet.topUp !== void 0;
|
|
12448
|
+
if (!deposit) {
|
|
12449
|
+
try {
|
|
12450
|
+
const record = key ? await wallet.book.base.get(key) : void 0;
|
|
12451
|
+
deposit = !record || typeof record.channelId !== "string" || record.pending !== void 0;
|
|
12452
|
+
} catch {
|
|
12453
|
+
deposit = true;
|
|
12454
|
+
}
|
|
12455
|
+
}
|
|
12456
|
+
if (!deposit) return paymentRequired;
|
|
12457
|
+
try {
|
|
12458
|
+
return withDepositBlockhash(paymentRequired, await finalizedBlockhash(this.init.rpcUrl, this.init.rpcHeaders));
|
|
12459
|
+
} catch {
|
|
12460
|
+
return paymentRequired;
|
|
12461
|
+
}
|
|
12462
|
+
}
|
|
12463
|
+
/**
|
|
12464
|
+
* Queue a re-read of a stored channel whose account is gone at finalized
|
|
12465
|
+
* and at confirmed commitment (closed, its account reclaimed), anchored at
|
|
12466
|
+
* the SDK's own `getBlockHeight` at confirmed. The anchor is saved in the
|
|
12467
|
+
* record (`reclaimAnchorHeight`), so a short-lived process finishes the
|
|
12468
|
+
* wait instead of starting it over; a failed read leaves resync to take it,
|
|
12469
|
+
* and a failed save only costs that. resync pays exact until finality is
|
|
12470
|
+
* LANDING_MARGIN_BLOCKS past the anchor, then drops the record if the
|
|
12471
|
+
* finalized chain still has no account (a lagging read finds the channel
|
|
12472
|
+
* and rewrites the record, without the anchor).
|
|
12473
|
+
*/
|
|
12474
|
+
async queueReclaimed(wallet, key, record, channelId, savedAnchor) {
|
|
12475
|
+
let anchorHeight = savedReclaimAnchor(record) ?? savedAnchor;
|
|
12476
|
+
if (anchorHeight === void 0) {
|
|
12477
|
+
try {
|
|
12478
|
+
anchorHeight = rpcInteger(
|
|
12479
|
+
await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
|
|
12480
|
+
"getBlockHeight"
|
|
12481
|
+
);
|
|
12482
|
+
} catch {
|
|
12483
|
+
}
|
|
12484
|
+
}
|
|
12485
|
+
if (anchorHeight !== void 0 && savedReclaimAnchor(record) !== anchorHeight) {
|
|
12486
|
+
try {
|
|
12487
|
+
await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
|
|
12488
|
+
} catch {
|
|
12489
|
+
}
|
|
12490
|
+
}
|
|
12491
|
+
addResyncTarget(wallet, {
|
|
12492
|
+
key,
|
|
12493
|
+
channelId,
|
|
12494
|
+
channelConfig: record.channelConfig,
|
|
12495
|
+
cumulative: parseAtomic(record.chargedCumulativeAmount),
|
|
12496
|
+
knownDeposit: parseAtomic(record.deposit),
|
|
12497
|
+
...anchorHeight !== void 0 ? { anchorHeight } : {},
|
|
12498
|
+
reason: "channel_unusable"
|
|
12499
|
+
});
|
|
12500
|
+
}
|
|
12501
|
+
/**
|
|
12502
|
+
* After the gateway refused an authorization with a plain 402 (no receipt,
|
|
12503
|
+
* not an admission refusal), check the channel it paid into. That is how
|
|
12504
|
+
* the gateway answers an authorization into a channel the facilitator
|
|
12505
|
+
* closed (its verify rejects it), and a channel with deposit to spare is
|
|
12506
|
+
* otherwise never read again: every call would sign another authorization
|
|
12507
|
+
* into it and pay exact. A channel the finalized chain shows closing,
|
|
12508
|
+
* closed or not this wallet's is re-read through resync (record dropped,
|
|
12509
|
+
* or kept and paid exact); one gone at every commitment is queued as
|
|
12510
|
+
* {@link queueReclaimed}; an open one of this wallet's, or one only at
|
|
12511
|
+
* confirmed (not finalized yet), is left as it is. A queued re-read is run
|
|
12512
|
+
* at once. Best effort: a failed read changes nothing, and the next
|
|
12513
|
+
* refusal checks again.
|
|
12514
|
+
*/
|
|
12515
|
+
async recheckRefusedChannel(wallet, key, savedAnchor) {
|
|
12516
|
+
try {
|
|
12517
|
+
const record = await wallet.book.get(key);
|
|
12518
|
+
if (!record || record.pending || typeof record.channelId !== "string" || wallet.resyncs.has(key)) return;
|
|
12519
|
+
const channelId = record.channelId;
|
|
12520
|
+
const channel = await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders);
|
|
12521
|
+
if (channel !== void 0) {
|
|
12522
|
+
if (channel.open && channel.payer === await this.init.address() && channel.authorizedSigner === record.channelConfig.payerAuthorizer && channel.mint === record.channelConfig.token) {
|
|
12523
|
+
return;
|
|
12524
|
+
}
|
|
12525
|
+
addResyncTarget(wallet, {
|
|
12526
|
+
key,
|
|
12527
|
+
channelId,
|
|
12528
|
+
channelConfig: record.channelConfig,
|
|
12529
|
+
cumulative: parseAtomic(record.chargedCumulativeAmount),
|
|
12530
|
+
knownDeposit: parseAtomic(record.deposit),
|
|
12531
|
+
reason: "channel_unusable"
|
|
12532
|
+
});
|
|
12533
|
+
} else {
|
|
12534
|
+
if (await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders, { commitment: "confirmed" }) !== void 0) return;
|
|
12535
|
+
await this.queueReclaimed(wallet, key, record, channelId, savedAnchor);
|
|
12536
|
+
}
|
|
12537
|
+
await this.resync(wallet);
|
|
12538
|
+
} catch {
|
|
12539
|
+
}
|
|
12540
|
+
}
|
|
12327
12541
|
/**
|
|
12328
12542
|
* When this payment will top up the wallet's channel, read how much of its
|
|
12329
12543
|
* deposit is already settled on-chain (at `finalized` commitment, so never
|
|
@@ -12377,31 +12591,7 @@ var SolanaBatchPayer = class {
|
|
|
12377
12591
|
}
|
|
12378
12592
|
if (channel === void 0 && unfinalized) return;
|
|
12379
12593
|
if (channel === void 0) {
|
|
12380
|
-
|
|
12381
|
-
if (anchorHeight === void 0) {
|
|
12382
|
-
try {
|
|
12383
|
-
anchorHeight = rpcInteger(
|
|
12384
|
-
await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
|
|
12385
|
-
"getBlockHeight"
|
|
12386
|
-
);
|
|
12387
|
-
} catch {
|
|
12388
|
-
}
|
|
12389
|
-
if (anchorHeight !== void 0) {
|
|
12390
|
-
try {
|
|
12391
|
-
await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
|
|
12392
|
-
} catch {
|
|
12393
|
-
}
|
|
12394
|
-
}
|
|
12395
|
-
}
|
|
12396
|
-
addResyncTarget(wallet, {
|
|
12397
|
-
key,
|
|
12398
|
-
channelId: record.channelId,
|
|
12399
|
-
channelConfig: record.channelConfig,
|
|
12400
|
-
cumulative: parseAtomic(record.chargedCumulativeAmount),
|
|
12401
|
-
knownDeposit: deposit,
|
|
12402
|
-
...anchorHeight !== void 0 ? { anchorHeight } : {},
|
|
12403
|
-
reason: "channel_unusable"
|
|
12404
|
-
});
|
|
12594
|
+
await this.queueReclaimed(wallet, key, record, record.channelId);
|
|
12405
12595
|
throw new ChannelResyncRequiredError(record.channelId);
|
|
12406
12596
|
}
|
|
12407
12597
|
if (!channel.open || channel.payer !== await this.init.address() || channel.authorizedSigner !== record.channelConfig.payerAuthorizer || channel.mint !== record.channelConfig.token) {
|
|
@@ -12576,7 +12766,6 @@ var SolanaBatchPayer = class {
|
|
|
12576
12766
|
// src/solana-client.ts
|
|
12577
12767
|
var SOLANA_API_URL = "https://sol.blockrun.ai/api";
|
|
12578
12768
|
var DEFAULT_MAX_TOKENS2 = 1024;
|
|
12579
|
-
var DEFAULT_TIMEOUT14 = 6e4;
|
|
12580
12769
|
var STALE_BLOCKHASH_RETRY_BACKOFFS_MS = [500, 2e3];
|
|
12581
12770
|
var MAX_PAYMENT_FAILURE_BYTES = 64 * 1024;
|
|
12582
12771
|
var DEFAULT_VIDEO_MODEL = "xai/grok-imagine-video";
|
|
@@ -12744,7 +12933,7 @@ var SolanaLLMClient = class {
|
|
|
12744
12933
|
const rpc = resolveRpcConfig(options.rpcUrl, options.rpcHeaders);
|
|
12745
12934
|
this.rpcUrl = rpc.url;
|
|
12746
12935
|
this.rpcHeaders = rpc.headers;
|
|
12747
|
-
this.timeout = options.timeout ||
|
|
12936
|
+
this.timeout = options.timeout || DEFAULT_TIMEOUT;
|
|
12748
12937
|
if (options.batch && !this.apiAuth) {
|
|
12749
12938
|
this.batchPayer = new SolanaBatchPayer({
|
|
12750
12939
|
options: options.batch,
|
|
@@ -13038,7 +13227,7 @@ var SolanaLLMClient = class {
|
|
|
13038
13227
|
body: requestBody
|
|
13039
13228
|
});
|
|
13040
13229
|
await this.assertPaid(paid);
|
|
13041
|
-
this.recordSettlement(costUsd);
|
|
13230
|
+
this.recordSettlement(costUsd, paid);
|
|
13042
13231
|
return paid;
|
|
13043
13232
|
} catch (error) {
|
|
13044
13233
|
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
@@ -13425,7 +13614,7 @@ var SolanaLLMClient = class {
|
|
|
13425
13614
|
closesAtStart
|
|
13426
13615
|
);
|
|
13427
13616
|
if (batch.kind === "paid") {
|
|
13428
|
-
this.recordSettlement(batch.chargedUsd);
|
|
13617
|
+
this.recordSettlement(batch.chargedUsd, batch.response);
|
|
13429
13618
|
const paid = await this.readPaidJson(batch.response);
|
|
13430
13619
|
return withCost(paid, batch.response, batch.chargedUsd, "batch");
|
|
13431
13620
|
}
|
|
@@ -13542,7 +13731,7 @@ var SolanaLLMClient = class {
|
|
|
13542
13731
|
};
|
|
13543
13732
|
const deliver = (data, response) => {
|
|
13544
13733
|
if (kind === "video") {
|
|
13545
|
-
if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd);
|
|
13734
|
+
if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd, response);
|
|
13546
13735
|
const txHash = response.headers.get("x-payment-receipt");
|
|
13547
13736
|
if (txHash) data.txHash = txHash;
|
|
13548
13737
|
}
|
|
@@ -13789,7 +13978,7 @@ var SolanaLLMClient = class {
|
|
|
13789
13978
|
throw withDisposition(error, "paid-or-in-doubt");
|
|
13790
13979
|
}
|
|
13791
13980
|
await this.assertPaid(retryResponse);
|
|
13792
|
-
|
|
13981
|
+
this.recordSettlement(costUsd, retryResponse);
|
|
13793
13982
|
const paid = await this.readPaidJson(retryResponse);
|
|
13794
13983
|
return withCost(paid, retryResponse, costUsd, "exact");
|
|
13795
13984
|
}
|
|
@@ -13849,11 +14038,11 @@ var SolanaLLMClient = class {
|
|
|
13849
14038
|
await this.assertPaid(retryResponse);
|
|
13850
14039
|
if (retryResponse.status === 202) {
|
|
13851
14040
|
const kind = pollKindFor(url);
|
|
13852
|
-
if (kind !== "video") this.recordSettlement(costUsd);
|
|
14041
|
+
if (kind !== "video") this.recordSettlement(costUsd, retryResponse);
|
|
13853
14042
|
const submitted = await retryResponse.json();
|
|
13854
14043
|
return this.followSolanaJob(submitted, media.pollBudgetMs ?? this.timeout, kind);
|
|
13855
14044
|
}
|
|
13856
|
-
this.recordSettlement(costUsd);
|
|
14045
|
+
this.recordSettlement(costUsd, retryResponse);
|
|
13857
14046
|
return retryResponse.json();
|
|
13858
14047
|
}
|
|
13859
14048
|
async getWithPaymentRaw(endpoint, params) {
|
|
@@ -13897,7 +14086,7 @@ var SolanaLLMClient = class {
|
|
|
13897
14086
|
}
|
|
13898
14087
|
});
|
|
13899
14088
|
await this.assertPaid(retryResponse);
|
|
13900
|
-
this.recordSettlement(costUsd);
|
|
14089
|
+
this.recordSettlement(costUsd, retryResponse);
|
|
13901
14090
|
return retryResponse.json();
|
|
13902
14091
|
}
|
|
13903
14092
|
/**
|
|
@@ -13935,8 +14124,17 @@ var SolanaLLMClient = class {
|
|
|
13935
14124
|
);
|
|
13936
14125
|
}
|
|
13937
14126
|
}
|
|
13938
|
-
/**
|
|
13939
|
-
|
|
14127
|
+
/**
|
|
14128
|
+
* Count one settled x402 payment against the session total.
|
|
14129
|
+
*
|
|
14130
|
+
* Takes the paid response so no booking site can forget the check: a
|
|
14131
|
+
* response whose settlement the gateway skipped ({@link settlementSkipped}:
|
|
14132
|
+
* `X-Settlement-Skipped: free-fallback | empty-result | unexecuted | …`, or
|
|
14133
|
+
* `X-Payment-Waived`) moved nothing on chain and books nothing.
|
|
14134
|
+
* `X-Payment-Settled: false` alone is still booked: the payment is owed.
|
|
14135
|
+
*/
|
|
14136
|
+
recordSettlement(costUsd, response) {
|
|
14137
|
+
if (settlementSkipped(response)) return;
|
|
13940
14138
|
this.sessionCalls += 1;
|
|
13941
14139
|
this.sessionTotalUsd += costUsd;
|
|
13942
14140
|
}
|