@blockrun/llm 3.24.8 → 3.24.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -423,13 +423,44 @@ function readSettlement(response, quotedUsd, scheme) {
423
423
  };
424
424
  return { costUsd, settlement };
425
425
  }
426
+ function settlementSkipReason(response) {
427
+ const headers = response.headers;
428
+ if (typeof headers?.get !== "function") return void 0;
429
+ const reason = [headers.get("X-Settlement-Skipped"), headers.get("X-Payment-Waived")].map((value) => value?.trim()).find((value) => !!value && !NOT_A_SKIP.test(value));
430
+ if (reason === void 0) return void 0;
431
+ const evidence = settlementEvidence(response);
432
+ if (evidence !== void 0) {
433
+ console.error(
434
+ `[@blockrun/llm] settlement: the gateway named a skipped settlement (${JSON.stringify(reason)}) on a response that also carries ${evidence}; booked as charged`
435
+ );
436
+ return void 0;
437
+ }
438
+ return reason;
439
+ }
440
+ var NOT_A_SKIP = /^(false|0|no|none)$/i;
441
+ function settlementEvidence(response) {
442
+ const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
443
+ const receipt = header ? decode(header) : void 0;
444
+ if (receipt && receipt.success !== false) {
445
+ if (typeof receipt.transaction === "string" && receipt.transaction) return "a settlement receipt with a transaction";
446
+ const extra = receipt.extra;
447
+ const charged = receiptChargedUsd({
448
+ amount: receipt.amount,
449
+ extra: extra && typeof extra === "object" ? extra : void 0
450
+ });
451
+ if (charged !== void 0 && charged > 0) return "a settlement receipt with a charge";
452
+ }
453
+ if (response.headers.get("X-Payment-Receipt")?.trim()) return "an X-Payment-Receipt transaction";
454
+ return void 0;
455
+ }
426
456
  function settlementSkipped(response) {
427
- return response.headers.get("X-Settlement-Skipped") === "free-fallback";
457
+ return settlementSkipReason(response) !== void 0;
428
458
  }
429
459
  function withCost(body, response, bookedUsd, scheme) {
430
- if (settlementSkipped(response)) {
460
+ const skipped = settlementSkipReason(response);
461
+ if (skipped !== void 0) {
431
462
  body.costUsd = 0;
432
- body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped: "free-fallback" };
463
+ body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped };
433
464
  return body;
434
465
  }
435
466
  body.costUsd = bookedUsd;
@@ -4958,7 +4989,7 @@ function getCostSummary() {
4958
4989
  }
4959
4990
 
4960
4991
  // src/version.ts
4961
- var SDK_VERSION = "3.24.8";
4992
+ var SDK_VERSION = "3.24.9";
4962
4993
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4963
4994
 
4964
4995
  // src/client.ts
@@ -5554,9 +5585,9 @@ var LLMClient = class _LLMClient {
5554
5585
  return this.bookPaid(url, body, details, retryResponse);
5555
5586
  }
5556
5587
  /**
5557
- * Book a served, paid chat call and attach its cost. A response the gateway
5558
- * served from a free fallback with settlement skipped
5559
- * (`X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
5588
+ * Book a served, paid chat call and attach its cost. A response whose
5589
+ * settlement the gateway skipped ({@link settlementSkipped}: e.g.
5590
+ * `X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
5560
5591
  * not added to `getSpending()` or the cost log, and reports `costUsd: 0`.
5561
5592
  */
5562
5593
  async bookPaid(url, body, details, response) {
@@ -5568,6 +5599,21 @@ var LLMClient = class _LLMClient {
5568
5599
  }
5569
5600
  return withCost(await this.parseChatResponse(response), response, costUsd, "exact");
5570
5601
  }
5602
+ /**
5603
+ * Book a served, paid raw (non-chat) call: the signed amount goes to
5604
+ * `getSpending()` and the cost log, unless the gateway skipped settlement
5605
+ * ({@link settlementSkipped}: e.g. `X-Settlement-Skipped: empty-result` on an
5606
+ * empty prediction-market search, `unexecuted` on a JSON-RPC call that never
5607
+ * ran, or `X-Payment-Waived`), in which case nothing moved and nothing is
5608
+ * booked. `X-Payment-Settled: false` alone is still booked: it is owed.
5609
+ */
5610
+ bookPaidRaw(url, details, response, body) {
5611
+ if (settlementSkipped(response)) return;
5612
+ const costUsd = parseFloat(details.amount) / 1e6;
5613
+ this.sessionCalls += 1;
5614
+ this.sessionTotalUsd += costUsd;
5615
+ this.recordCost(url, costUsd, body ? { body, network: details.network } : { network: details.network });
5616
+ }
5571
5617
  /**
5572
5618
  * Sign a payment header and return the PAYMENT-SIGNATURE value.
5573
5619
  * Extracted to share logic between streaming and non-streaming flows.
@@ -5668,8 +5714,10 @@ var LLMClient = class _LLMClient {
5668
5714
  throw withDisposition(err, "paid-or-in-doubt");
5669
5715
  }
5670
5716
  if (preAuthResp.ok) {
5671
- this.sessionCalls += 1;
5672
- this.sessionTotalUsd += signed.costUsd;
5717
+ if (!settlementSkipped(preAuthResp)) {
5718
+ this.sessionCalls += 1;
5719
+ this.sessionTotalUsd += signed.costUsd;
5720
+ }
5673
5721
  return preAuthResp;
5674
5722
  }
5675
5723
  this.preAuthCache.delete(cacheKey2);
@@ -5734,8 +5782,10 @@ var LLMClient = class _LLMClient {
5734
5782
  }
5735
5783
  throw new APIError(`API error after payment: ${streamResp.status}`, streamResp.status, sanitizeErrorResponse(errorBody));
5736
5784
  }
5737
- this.sessionCalls += 1;
5738
- this.sessionTotalUsd += costUsd;
5785
+ if (!settlementSkipped(streamResp)) {
5786
+ this.sessionCalls += 1;
5787
+ this.sessionTotalUsd += costUsd;
5788
+ }
5739
5789
  return streamResp;
5740
5790
  }
5741
5791
  /**
@@ -5859,10 +5909,7 @@ var LLMClient = class _LLMClient {
5859
5909
  }
5860
5910
  throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
5861
5911
  }
5862
- const costUsd2 = parseFloat(details.amount) / 1e6;
5863
- this.sessionCalls += 1;
5864
- this.sessionTotalUsd += costUsd2;
5865
- this.recordCost(url, costUsd2, { body, network: details.network });
5912
+ this.bookPaidRaw(url, details, retryResp2, body);
5866
5913
  return retryResp2.json();
5867
5914
  }
5868
5915
  }
@@ -5882,10 +5929,7 @@ var LLMClient = class _LLMClient {
5882
5929
  sanitizeErrorResponse(errorBody)
5883
5930
  );
5884
5931
  }
5885
- const costUsd = parseFloat(details.amount) / 1e6;
5886
- this.sessionCalls += 1;
5887
- this.sessionTotalUsd += costUsd;
5888
- this.recordCost(url, costUsd, { body, network: details.network });
5932
+ this.bookPaidRaw(url, details, retryResponse, body);
5889
5933
  return retryResponse.json();
5890
5934
  }
5891
5935
  /**
@@ -6005,10 +6049,7 @@ var LLMClient = class _LLMClient {
6005
6049
  }
6006
6050
  throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
6007
6051
  }
6008
- const costUsd2 = parseFloat(details.amount) / 1e6;
6009
- this.sessionCalls += 1;
6010
- this.sessionTotalUsd += costUsd2;
6011
- this.recordCost(url, costUsd2, { network: details.network });
6052
+ this.bookPaidRaw(url, details, retryResp2);
6012
6053
  return retryResp2.json();
6013
6054
  }
6014
6055
  }
@@ -6028,10 +6069,7 @@ var LLMClient = class _LLMClient {
6028
6069
  sanitizeErrorResponse(errorBody)
6029
6070
  );
6030
6071
  }
6031
- const costUsd = parseFloat(details.amount) / 1e6;
6032
- this.sessionCalls += 1;
6033
- this.sessionTotalUsd += costUsd;
6034
- this.recordCost(url, costUsd, { network: details.network });
6072
+ this.bookPaidRaw(url, details, retryResponse);
6035
6073
  return retryResponse.json();
6036
6074
  }
6037
6075
  /**
@@ -7044,8 +7082,10 @@ var MusicClient = class {
7044
7082
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
7045
7083
  }
7046
7084
  const data = await retryResponse.json();
7047
- this.sessionCalls++;
7048
- this.sessionTotalUsd += 0.1575;
7085
+ if (!settlementSkipped(retryResponse)) {
7086
+ this.sessionCalls++;
7087
+ this.sessionTotalUsd += 0.1575;
7088
+ }
7049
7089
  const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
7050
7090
  if (txHash) data.txHash = txHash;
7051
7091
  return data;
@@ -7257,9 +7297,11 @@ var SpeechClient = class {
7257
7297
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
7258
7298
  }
7259
7299
  const data = await retryResponse.json();
7260
- this.sessionCalls++;
7261
- const paidUsd = Number(details.amount) / 1e6;
7262
- if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
7300
+ if (!settlementSkipped(retryResponse)) {
7301
+ this.sessionCalls++;
7302
+ const paidUsd = Number(details.amount) / 1e6;
7303
+ if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
7304
+ }
7263
7305
  const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
7264
7306
  if (txHash) data.txHash = txHash;
7265
7307
  return data;
@@ -7556,8 +7598,10 @@ var VideoClient = class {
7556
7598
  }
7557
7599
  if (lastStatus === "completed") {
7558
7600
  const data = pollData;
7559
- this.sessionCalls++;
7560
- this.sessionTotalUsd += Number(details.amount) / 1e6;
7601
+ if (!settlementSkipped(pollResp)) {
7602
+ this.sessionCalls++;
7603
+ this.sessionTotalUsd += Number(details.amount) / 1e6;
7604
+ }
7561
7605
  const txHash = pollResp.headers.get("x-payment-receipt") || pollResp.headers.get("X-Payment-Receipt");
7562
7606
  if (txHash) data.txHash = txHash;
7563
7607
  return data;
@@ -7852,8 +7896,10 @@ var VoiceClient = class {
7852
7896
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
7853
7897
  }
7854
7898
  const data = await retryResponse.json();
7855
- this.sessionCalls++;
7856
- this.sessionTotalUsd += CALL_PRICE_USD;
7899
+ if (!settlementSkipped(retryResponse)) {
7900
+ this.sessionCalls++;
7901
+ this.sessionTotalUsd += CALL_PRICE_USD;
7902
+ }
7857
7903
  const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
7858
7904
  if (txHash) data.txHash = txHash;
7859
7905
  return data;
@@ -8073,9 +8119,10 @@ var PhoneClient = class {
8073
8119
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
8074
8120
  }
8075
8121
  const data = await this.unwrap(retry, true);
8076
- const costUsd = Number.parseFloat(details.amount) / 1e6;
8077
- this.sessionCalls += 1;
8078
- this.sessionTotalUsd += costUsd;
8122
+ if (!settlementSkipped(retry)) {
8123
+ this.sessionCalls += 1;
8124
+ this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
8125
+ }
8079
8126
  const txHash = retry.headers.get("x-payment-receipt") || retry.headers.get("X-Payment-Receipt");
8080
8127
  if (txHash && data && typeof data === "object" && !("txHash" in data)) {
8081
8128
  data.txHash = txHash;
@@ -8240,9 +8287,10 @@ var PortraitClient = class {
8240
8287
  throw new PaymentError("Payment was rejected. Check your wallet balance.");
8241
8288
  }
8242
8289
  const data = await this.unwrap(retry, true);
8243
- const costUsd = Number.parseFloat(details.amount) / 1e6;
8244
- this.sessionCalls += 1;
8245
- this.sessionTotalUsd += costUsd;
8290
+ if (!settlementSkipped(retry)) {
8291
+ this.sessionCalls += 1;
8292
+ this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
8293
+ }
8246
8294
  if (data && typeof data === "object" && !("txHash" in data)) {
8247
8295
  const settlement = data.settlement;
8248
8296
  if (settlement?.tx_hash) {
@@ -9059,9 +9107,11 @@ var RpcClient = class {
9059
9107
  }
9060
9108
  throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
9061
9109
  }
9062
- this.sessionCalls++;
9063
- const paidUsd = Number(details.amount) / 1e6;
9064
- if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
9110
+ if (!settlementSkipped(retryResponse)) {
9111
+ this.sessionCalls++;
9112
+ const paidUsd = Number(details.amount) / 1e6;
9113
+ if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
9114
+ }
9065
9115
  return { data: await retryResponse.json(), headers: retryResponse.headers };
9066
9116
  }
9067
9117
  async fetchWithTimeout(url, options) {
@@ -9208,7 +9258,7 @@ var BlockrunClient = class {
9208
9258
  if (resp402.status !== 402) {
9209
9259
  await this.throwApiError(resp402, `poll submit failed (${submitUrl})`);
9210
9260
  }
9211
- const paymentPayload = await this.signFrom402(resp402, submitUrl, {
9261
+ const { paymentPayload, costUsd } = await this.signFrom402(resp402, submitUrl, {
9212
9262
  description: "BlockRun async job",
9213
9263
  maxTimeoutSeconds: MAX_SIGNED_AUTH_SECONDS
9214
9264
  });
@@ -9227,7 +9277,7 @@ var BlockrunClient = class {
9227
9277
  await this.throwApiError(submitResp, `poll submit failed (${submitUrl})`);
9228
9278
  }
9229
9279
  if (submitResp.status === 200) {
9230
- this.recordSpending();
9280
+ this.recordSpending(submitResp, costUsd);
9231
9281
  return submitResp.json();
9232
9282
  }
9233
9283
  const submitData = await submitResp.json();
@@ -9264,7 +9314,7 @@ var BlockrunClient = class {
9264
9314
  );
9265
9315
  }
9266
9316
  if (pollResp.status === 200 && lastStatus === "completed") {
9267
- this.recordSpending();
9317
+ this.recordSpending(pollResp, costUsd);
9268
9318
  return pollData;
9269
9319
  }
9270
9320
  if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
@@ -9305,7 +9355,7 @@ var BlockrunClient = class {
9305
9355
  if (resp402.status === 200) {
9306
9356
  streamResp = resp402;
9307
9357
  } else if (resp402.status === 402) {
9308
- const paymentPayload = await this.signFrom402(resp402, url, {
9358
+ const { paymentPayload, costUsd } = await this.signFrom402(resp402, url, {
9309
9359
  description: "BlockRun stream"
9310
9360
  });
9311
9361
  streamResp = await this.fetchWithTimeout(url, {
@@ -9324,7 +9374,7 @@ var BlockrunClient = class {
9324
9374
  if (!streamResp.ok) {
9325
9375
  await this.throwApiError(streamResp, `stream failed after payment (${url})`);
9326
9376
  }
9327
- this.recordSpending();
9377
+ this.recordSpending(streamResp, costUsd);
9328
9378
  } else {
9329
9379
  await this.throwApiError(resp402, `stream failed (${url})`);
9330
9380
  return;
@@ -9380,7 +9430,7 @@ var BlockrunClient = class {
9380
9430
  return response.json();
9381
9431
  }
9382
9432
  async handlePaymentAndRetry(url, method, body, response) {
9383
- const paymentPayload = await this.signFrom402(response, url, {
9433
+ const { paymentPayload, costUsd } = await this.signFrom402(response, url, {
9384
9434
  description: "BlockRun"
9385
9435
  });
9386
9436
  const retryInit = {
@@ -9403,13 +9453,14 @@ var BlockrunClient = class {
9403
9453
  if (!retry.ok) {
9404
9454
  await this.throwApiError(retry, `${method} ${url} failed after payment`);
9405
9455
  }
9406
- this.recordSpending();
9456
+ this.recordSpending(retry, costUsd);
9407
9457
  return retry.json();
9408
9458
  }
9409
9459
  /**
9410
9460
  * Read a 402 response's payment requirements (header or body), then sign and
9411
- * return the base64 PAYMENT-SIGNATURE payload. Also records the cost-to-be
9412
- * onto the response context (settled on `recordSpending`).
9461
+ * return the base64 PAYMENT-SIGNATURE payload with what it pays, in USD.
9462
+ * The cost stays with the call that signed it (booked by `recordSpending`):
9463
+ * concurrent calls on one client never share it.
9413
9464
  */
9414
9465
  async signFrom402(response, url, opts) {
9415
9466
  let paymentHeader = response.headers.get("payment-required");
@@ -9427,8 +9478,8 @@ var BlockrunClient = class {
9427
9478
  }
9428
9479
  const paymentRequired = parsePaymentRequired(paymentHeader);
9429
9480
  const details = extractPaymentDetails(paymentRequired);
9430
- this.pendingCostUsd = parseFloat(details.amount) / 1e6;
9431
- return createPaymentPayload(
9481
+ const costUsd = parseFloat(details.amount) / 1e6;
9482
+ const paymentPayload = await createPaymentPayload(
9432
9483
  this.privateKey,
9433
9484
  this.account.address,
9434
9485
  details.recipient,
@@ -9445,14 +9496,20 @@ var BlockrunClient = class {
9445
9496
  asset: details.asset
9446
9497
  }
9447
9498
  );
9499
+ return { paymentPayload, costUsd };
9448
9500
  }
9449
- /** Accumulates the most-recent pending cost; settled by recordSpending. */
9450
- pendingCostUsd = 0;
9451
- recordSpending() {
9452
- if (this.pendingCostUsd > 0) {
9501
+ /**
9502
+ * Book `costUsd`, what the call that `response` answered signed. A response
9503
+ * whose settlement the gateway skipped ({@link settlementSkipped}:
9504
+ * `X-Settlement-Skipped: <reason>` or `X-Payment-Waived`) moved nothing, so
9505
+ * nothing is booked. `X-Payment-Settled: false` alone is still booked: the
9506
+ * payment is owed.
9507
+ */
9508
+ recordSpending(response, costUsd) {
9509
+ if (settlementSkipped(response)) return;
9510
+ if (costUsd > 0) {
9453
9511
  this.sessionCalls += 1;
9454
- this.sessionTotalUsd += this.pendingCostUsd;
9455
- this.pendingCostUsd = 0;
9512
+ this.sessionTotalUsd += costUsd;
9456
9513
  }
9457
9514
  }
9458
9515
  async throwApiError(resp, prefix) {
@@ -11671,6 +11728,8 @@ var SolanaBatchPayer = class {
11671
11728
  wallet.refusedFor = outcome.errorReason;
11672
11729
  }
11673
11730
  }
11731
+ } else if (built.key && outcome.reason === "payment_required" && outcome.refusedForMs === void 0) {
11732
+ await this.recheckRefusedChannel(wallet, built.key, built.reclaimAnchor);
11674
11733
  }
11675
11734
  sent = void 0;
11676
11735
  release();
@@ -11938,6 +11997,13 @@ var SolanaBatchPayer = class {
11938
11997
  }
11939
11998
  /** Build a new batch payment for a 402: the payload, its headers and its kind. */
11940
11999
  async buildPayment(wallet, paymentRequired) {
12000
+ const acceptKey = channelKeyOf(trustedBatchAccept(paymentRequired, this.init.options.operators));
12001
+ let reclaimAnchor;
12002
+ try {
12003
+ const stored = acceptKey ? await wallet.book.base.get(acceptKey) : void 0;
12004
+ reclaimAnchor = stored ? savedReclaimAnchor(stored) : void 0;
12005
+ } catch {
12006
+ }
11941
12007
  const { http, payload } = await this.createPayload(wallet, paymentRequired);
11942
12008
  const scope = payScope.getStore();
11943
12009
  if (scope?.afterError) {
@@ -11959,7 +12025,16 @@ var SolanaBatchPayer = class {
11959
12025
  const record = known?.record;
11960
12026
  kind = record && (record.pending === void 0 || record.hasConfirmedState) ? "top-up" : "open";
11961
12027
  }
11962
- return { http, payload, headers, kind, key: known?.key, sentAt: Date.now(), sends: 0 };
12028
+ return {
12029
+ http,
12030
+ payload,
12031
+ headers,
12032
+ kind,
12033
+ key: known?.key,
12034
+ ...reclaimAnchor !== void 0 && known?.key === acceptKey ? { reclaimAnchor } : {},
12035
+ sentAt: Date.now(),
12036
+ sends: 0
12037
+ };
11963
12038
  }
11964
12039
  /**
11965
12040
  * Write a deposit's intent to the wallet's journal before it is sent
@@ -12193,6 +12268,84 @@ var SolanaBatchPayer = class {
12193
12268
  }
12194
12269
  }
12195
12270
  }
12271
+ /**
12272
+ * Queue a re-read of a stored channel whose account is gone at finalized
12273
+ * and at confirmed commitment (closed, its account reclaimed), anchored at
12274
+ * the SDK's own `getBlockHeight` at confirmed. The anchor is saved in the
12275
+ * record (`reclaimAnchorHeight`), so a short-lived process finishes the
12276
+ * wait instead of starting it over; a failed read leaves resync to take it,
12277
+ * and a failed save only costs that. resync pays exact until finality is
12278
+ * LANDING_MARGIN_BLOCKS past the anchor, then drops the record if the
12279
+ * finalized chain still has no account (a lagging read finds the channel
12280
+ * and rewrites the record, without the anchor).
12281
+ */
12282
+ async queueReclaimed(wallet, key, record, channelId, savedAnchor) {
12283
+ let anchorHeight = savedReclaimAnchor(record) ?? savedAnchor;
12284
+ if (anchorHeight === void 0) {
12285
+ try {
12286
+ anchorHeight = rpcInteger(
12287
+ await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
12288
+ "getBlockHeight"
12289
+ );
12290
+ } catch {
12291
+ }
12292
+ }
12293
+ if (anchorHeight !== void 0 && savedReclaimAnchor(record) !== anchorHeight) {
12294
+ try {
12295
+ await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
12296
+ } catch {
12297
+ }
12298
+ }
12299
+ addResyncTarget(wallet, {
12300
+ key,
12301
+ channelId,
12302
+ channelConfig: record.channelConfig,
12303
+ cumulative: parseAtomic(record.chargedCumulativeAmount),
12304
+ knownDeposit: parseAtomic(record.deposit),
12305
+ ...anchorHeight !== void 0 ? { anchorHeight } : {},
12306
+ reason: "channel_unusable"
12307
+ });
12308
+ }
12309
+ /**
12310
+ * After the gateway refused an authorization with a plain 402 (no receipt,
12311
+ * not an admission refusal), check the channel it paid into. That is how
12312
+ * the gateway answers an authorization into a channel the facilitator
12313
+ * closed (its verify rejects it), and a channel with deposit to spare is
12314
+ * otherwise never read again: every call would sign another authorization
12315
+ * into it and pay exact. A channel the finalized chain shows closing,
12316
+ * closed or not this wallet's is re-read through resync (record dropped,
12317
+ * or kept and paid exact); one gone at every commitment is queued as
12318
+ * {@link queueReclaimed}; an open one of this wallet's, or one only at
12319
+ * confirmed (not finalized yet), is left as it is. A queued re-read is run
12320
+ * at once. Best effort: a failed read changes nothing, and the next
12321
+ * refusal checks again.
12322
+ */
12323
+ async recheckRefusedChannel(wallet, key, savedAnchor) {
12324
+ try {
12325
+ const record = await wallet.book.get(key);
12326
+ if (!record || record.pending || typeof record.channelId !== "string" || wallet.resyncs.has(key)) return;
12327
+ const channelId = record.channelId;
12328
+ const channel = await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders);
12329
+ if (channel !== void 0) {
12330
+ if (channel.open && channel.payer === await this.init.address() && channel.authorizedSigner === record.channelConfig.payerAuthorizer && channel.mint === record.channelConfig.token) {
12331
+ return;
12332
+ }
12333
+ addResyncTarget(wallet, {
12334
+ key,
12335
+ channelId,
12336
+ channelConfig: record.channelConfig,
12337
+ cumulative: parseAtomic(record.chargedCumulativeAmount),
12338
+ knownDeposit: parseAtomic(record.deposit),
12339
+ reason: "channel_unusable"
12340
+ });
12341
+ } else {
12342
+ if (await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders, { commitment: "confirmed" }) !== void 0) return;
12343
+ await this.queueReclaimed(wallet, key, record, channelId, savedAnchor);
12344
+ }
12345
+ await this.resync(wallet);
12346
+ } catch {
12347
+ }
12348
+ }
12196
12349
  /**
12197
12350
  * When this payment will top up the wallet's channel, read how much of its
12198
12351
  * deposit is already settled on-chain (at `finalized` commitment, so never
@@ -12246,31 +12399,7 @@ var SolanaBatchPayer = class {
12246
12399
  }
12247
12400
  if (channel === void 0 && unfinalized) return;
12248
12401
  if (channel === void 0) {
12249
- let anchorHeight = savedReclaimAnchor(record);
12250
- if (anchorHeight === void 0) {
12251
- try {
12252
- anchorHeight = rpcInteger(
12253
- await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
12254
- "getBlockHeight"
12255
- );
12256
- } catch {
12257
- }
12258
- if (anchorHeight !== void 0) {
12259
- try {
12260
- await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
12261
- } catch {
12262
- }
12263
- }
12264
- }
12265
- addResyncTarget(wallet, {
12266
- key,
12267
- channelId: record.channelId,
12268
- channelConfig: record.channelConfig,
12269
- cumulative: parseAtomic(record.chargedCumulativeAmount),
12270
- knownDeposit: deposit,
12271
- ...anchorHeight !== void 0 ? { anchorHeight } : {},
12272
- reason: "channel_unusable"
12273
- });
12402
+ await this.queueReclaimed(wallet, key, record, record.channelId);
12274
12403
  throw new ChannelResyncRequiredError(record.channelId);
12275
12404
  }
12276
12405
  if (!channel.open || channel.payer !== await this.init.address() || channel.authorizedSigner !== record.channelConfig.payerAuthorizer || channel.mint !== record.channelConfig.token) {
@@ -12907,7 +13036,7 @@ var SolanaLLMClient = class {
12907
13036
  body: requestBody
12908
13037
  });
12909
13038
  await this.assertPaid(paid);
12910
- this.recordSettlement(costUsd);
13039
+ this.recordSettlement(costUsd, paid);
12911
13040
  return paid;
12912
13041
  } catch (error) {
12913
13042
  if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
@@ -13294,7 +13423,7 @@ var SolanaLLMClient = class {
13294
13423
  closesAtStart
13295
13424
  );
13296
13425
  if (batch.kind === "paid") {
13297
- this.recordSettlement(batch.chargedUsd);
13426
+ this.recordSettlement(batch.chargedUsd, batch.response);
13298
13427
  const paid = await this.readPaidJson(batch.response);
13299
13428
  return withCost(paid, batch.response, batch.chargedUsd, "batch");
13300
13429
  }
@@ -13411,7 +13540,7 @@ var SolanaLLMClient = class {
13411
13540
  };
13412
13541
  const deliver = (data, response) => {
13413
13542
  if (kind === "video") {
13414
- if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd);
13543
+ if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd, response);
13415
13544
  const txHash = response.headers.get("x-payment-receipt");
13416
13545
  if (txHash) data.txHash = txHash;
13417
13546
  }
@@ -13658,7 +13787,7 @@ var SolanaLLMClient = class {
13658
13787
  throw withDisposition(error, "paid-or-in-doubt");
13659
13788
  }
13660
13789
  await this.assertPaid(retryResponse);
13661
- if (!settlementSkipped(retryResponse)) this.recordSettlement(costUsd);
13790
+ this.recordSettlement(costUsd, retryResponse);
13662
13791
  const paid = await this.readPaidJson(retryResponse);
13663
13792
  return withCost(paid, retryResponse, costUsd, "exact");
13664
13793
  }
@@ -13718,11 +13847,11 @@ var SolanaLLMClient = class {
13718
13847
  await this.assertPaid(retryResponse);
13719
13848
  if (retryResponse.status === 202) {
13720
13849
  const kind = pollKindFor(url);
13721
- if (kind !== "video") this.recordSettlement(costUsd);
13850
+ if (kind !== "video") this.recordSettlement(costUsd, retryResponse);
13722
13851
  const submitted = await retryResponse.json();
13723
13852
  return this.followSolanaJob(submitted, media.pollBudgetMs ?? this.timeout, kind);
13724
13853
  }
13725
- this.recordSettlement(costUsd);
13854
+ this.recordSettlement(costUsd, retryResponse);
13726
13855
  return retryResponse.json();
13727
13856
  }
13728
13857
  async getWithPaymentRaw(endpoint, params) {
@@ -13766,7 +13895,7 @@ var SolanaLLMClient = class {
13766
13895
  }
13767
13896
  });
13768
13897
  await this.assertPaid(retryResponse);
13769
- this.recordSettlement(costUsd);
13898
+ this.recordSettlement(costUsd, retryResponse);
13770
13899
  return retryResponse.json();
13771
13900
  }
13772
13901
  /**
@@ -13804,8 +13933,17 @@ var SolanaLLMClient = class {
13804
13933
  );
13805
13934
  }
13806
13935
  }
13807
- /** Count one settled x402 payment against the session total. */
13808
- recordSettlement(costUsd) {
13936
+ /**
13937
+ * Count one settled x402 payment against the session total.
13938
+ *
13939
+ * Takes the paid response so no booking site can forget the check: a
13940
+ * response whose settlement the gateway skipped ({@link settlementSkipped}:
13941
+ * `X-Settlement-Skipped: free-fallback | empty-result | unexecuted | …`, or
13942
+ * `X-Payment-Waived`) moved nothing on chain and books nothing.
13943
+ * `X-Payment-Settled: false` alone is still booked: the payment is owed.
13944
+ */
13945
+ recordSettlement(costUsd, response) {
13946
+ if (settlementSkipped(response)) return;
13809
13947
  this.sessionCalls += 1;
13810
13948
  this.sessionTotalUsd += costUsd;
13811
13949
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/llm",
3
- "version": "3.24.8",
3
+ "version": "3.24.9",
4
4
  "type": "module",
5
5
  "description": "TypeScript SDK for BlockRun - every frontier model behind one OpenAI-compatible client. Pay with a BlockRun API key or per-call in USDC. Smart routing picks the cheapest capable model. No rate limits.",
6
6
  "main": "dist/index.cjs",