@blockrun/llm 3.21.0 → 3.22.0

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.cjs CHANGED
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  BatchPaymentUnresolvedError: () => BatchPaymentUnresolvedError,
41
41
  BlockrunClient: () => BlockrunClient,
42
42
  BlockrunError: () => BlockrunError,
43
+ DEFAULT_BATCH_IN_DOUBT: () => DEFAULT_BATCH_IN_DOUBT,
43
44
  DEFAULT_BATCH_RATE_LIMIT: () => DEFAULT_BATCH_RATE_LIMIT,
44
45
  EVM_NETWORKS: () => EVM_NETWORKS,
45
46
  ImageClient: () => ImageClient,
@@ -103,6 +104,7 @@ __export(index_exports, {
103
104
  loadWallet: () => loadWallet,
104
105
  logCost: () => logCost,
105
106
  parsePaymentRequired: () => parsePaymentRequired,
107
+ readSettlement: () => readSettlement,
106
108
  retryDisposition: () => retryDisposition,
107
109
  saveSolanaWallet: () => saveSolanaWallet,
108
110
  saveToCache: () => saveToCache,
@@ -117,6 +119,8 @@ __export(index_exports, {
117
119
  solanaKeyToBytes: () => solanaKeyToBytes,
118
120
  solanaPublicKey: () => solanaPublicKey,
119
121
  status: () => status,
122
+ toWireSearchParameters: () => toWireSearchParameters,
123
+ toWireTools: () => toWireTools,
120
124
  validateMaxTokens: () => validateMaxTokens,
121
125
  validateModel: () => validateModel,
122
126
  validateTemperature: () => validateTemperature,
@@ -439,6 +443,122 @@ var ApiKeyAuth = class {
439
443
  }
440
444
  };
441
445
 
446
+ // src/search-wire.ts
447
+ function put(out, key, value) {
448
+ if (value !== void 0) out[key] = value;
449
+ }
450
+ function handles(list) {
451
+ return list?.map((h) => h.trim().replace(/^@+/, ""));
452
+ }
453
+ function sourceToWire(source) {
454
+ const raw = source;
455
+ const out = { ...raw };
456
+ for (const key of Object.keys(out)) if (/[A-Z]/.test(key)) delete out[key];
457
+ switch (source.type) {
458
+ case "x":
459
+ put(out, "included_x_handles", handles(source.includedXHandles) ?? out.included_x_handles);
460
+ put(out, "excluded_x_handles", handles(source.excludedXHandles) ?? out.excluded_x_handles);
461
+ put(out, "post_favorite_count", source.postFavoriteCount);
462
+ put(out, "post_view_count", source.postViewCount);
463
+ put(out, "enable_image_understanding", source.enableImageUnderstanding);
464
+ put(out, "enable_video_understanding", source.enableVideoUnderstanding);
465
+ break;
466
+ case "web":
467
+ put(out, "country", source.country);
468
+ put(out, "allowed_websites", source.allowedWebsites);
469
+ put(out, "excluded_websites", source.excludedWebsites);
470
+ put(out, "safe_search", source.safeSearch);
471
+ put(out, "enable_image_understanding", source.enableImageUnderstanding);
472
+ break;
473
+ case "news":
474
+ put(out, "country", source.country);
475
+ put(out, "allowed_websites", source.allowedWebsites);
476
+ put(out, "excluded_websites", source.excludedWebsites);
477
+ put(out, "safe_search", source.safeSearch);
478
+ break;
479
+ case "rss":
480
+ put(out, "links", source.links);
481
+ break;
482
+ }
483
+ return out;
484
+ }
485
+ function toWireSearchParameters(sp) {
486
+ const raw = sp;
487
+ const out = {};
488
+ for (const [key, value] of Object.entries(raw)) if (!/[A-Z]/.test(key) && key !== "sources") out[key] = value;
489
+ put(out, "mode", sp.mode);
490
+ if (sp.sources) out.sources = sp.sources.map(sourceToWire);
491
+ put(out, "return_citations", sp.returnCitations);
492
+ put(out, "from_date", sp.fromDate);
493
+ put(out, "to_date", sp.toDate);
494
+ put(out, "max_search_results", sp.maxSearchResults);
495
+ put(out, "max_turns", sp.maxTurns);
496
+ return out;
497
+ }
498
+ function toWireTools(tools) {
499
+ return tools.map((tool) => {
500
+ if (tool.type === "x_search") {
501
+ const out = { type: "x_search" };
502
+ put(out, "allowed_x_handles", handles(tool.allowedXHandles));
503
+ put(out, "excluded_x_handles", handles(tool.excludedXHandles));
504
+ put(out, "from_date", tool.fromDate);
505
+ put(out, "to_date", tool.toDate);
506
+ put(out, "enable_image_understanding", tool.enableImageUnderstanding);
507
+ put(out, "enable_video_understanding", tool.enableVideoUnderstanding);
508
+ return out;
509
+ }
510
+ if (tool.type === "web_search") {
511
+ const out = { type: "web_search" };
512
+ put(out, "allowed_domains", tool.allowedDomains);
513
+ put(out, "excluded_domains", tool.excludedDomains);
514
+ put(out, "enable_image_understanding", tool.enableImageUnderstanding);
515
+ return out;
516
+ }
517
+ return tool;
518
+ });
519
+ }
520
+
521
+ // src/receipt.ts
522
+ function atomicToUsd(amount) {
523
+ return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
524
+ }
525
+ function receiptChargedUsd(receipt) {
526
+ return atomicToUsd(receipt?.extra?.chargedAmount) ?? atomicToUsd(receipt?.amount);
527
+ }
528
+ function decode(header) {
529
+ try {
530
+ const json = typeof Buffer !== "undefined" ? Buffer.from(header, "base64").toString("utf8") : atob(header);
531
+ const parsed = JSON.parse(json);
532
+ return parsed && typeof parsed === "object" ? parsed : void 0;
533
+ } catch {
534
+ return void 0;
535
+ }
536
+ }
537
+ function readSettlement(response, quotedUsd, scheme) {
538
+ const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
539
+ const receipt = header ? decode(header) : void 0;
540
+ const extra = receipt?.extra;
541
+ const costUsd = receiptChargedUsd({
542
+ amount: receipt?.amount,
543
+ extra: extra && typeof extra === "object" ? extra : void 0
544
+ }) ?? quotedUsd;
545
+ const receiptTx = typeof receipt?.transaction === "string" && receipt.transaction ? receipt.transaction : void 0;
546
+ const transaction = receiptTx ?? (response.headers.get("X-Payment-Receipt")?.trim() || void 0);
547
+ const settlement = {
548
+ scheme,
549
+ quotedUsd,
550
+ ...transaction ? { transaction } : {},
551
+ ...typeof receipt?.network === "string" ? { network: receipt.network } : {},
552
+ ...receipt?.success === false ? { success: false } : {}
553
+ };
554
+ return { costUsd, settlement };
555
+ }
556
+ function withCost(body, response, bookedUsd, scheme) {
557
+ body.costUsd = bookedUsd;
558
+ body.settlement = readSettlement(response, bookedUsd, scheme).settlement;
559
+ return body;
560
+ }
561
+
442
562
  // src/client.ts
443
563
  var import_accounts2 = require("viem/accounts");
444
564
 
@@ -4225,6 +4345,11 @@ function errSummary(err) {
4225
4345
  }
4226
4346
  return String(err).slice(0, 100);
4227
4347
  }
4348
+ function requestsSearch(options) {
4349
+ if (options.tools?.some((tool) => tool.type === "x_search" || tool.type === "web_search")) return true;
4350
+ if (options.searchParameters !== void 0) return options.searchParameters.mode !== "off";
4351
+ return options.search === true;
4352
+ }
4228
4353
  function routingProfileForModel(model) {
4229
4354
  return AUTO_ROUTING_PROFILES[model.toLowerCase()];
4230
4355
  }
@@ -4252,7 +4377,7 @@ function routingText(messages) {
4252
4377
  };
4253
4378
  }
4254
4379
  function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, options = {}) {
4255
- const tools = options.tools ?? [];
4380
+ const tools = (options.tools ?? []).filter((tool) => tool.type === "function");
4256
4381
  const requiresTools = options.toolChoice === "none" ? false : options.toolChoice === "required" || typeof options.toolChoice === "object" ? true : void 0;
4257
4382
  const decision = route(prompt, systemPrompt, maxOutputTokens, {
4258
4383
  config: DEFAULT_ROUTING_CONFIG,
@@ -4272,6 +4397,15 @@ function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, o
4272
4397
  const resolved = !id.startsWith("free/") ? id : modelPricing.has(`nvidia/${id.slice(5)}`) ? `nvidia/${id.slice(5)}` : null;
4273
4398
  if (resolved && !callable.includes(resolved)) callable.push(resolved);
4274
4399
  }
4400
+ if (requestsSearch(options)) {
4401
+ const searchable = callable.filter((id) => id.startsWith("xai/"));
4402
+ if (searchable.length === 0) {
4403
+ throw new Error(
4404
+ `Search (x_search / web_search tools or searchParameters) runs on xai/* models only, and the ${options.routingProfile ?? "auto"} route has no xai/* candidate for this request. Call an xai/* model directly (e.g. xai/grok-4.3) or drop the search options.`
4405
+ );
4406
+ }
4407
+ callable.splice(0, callable.length, ...searchable);
4408
+ }
4275
4409
  const estimatedInputTokens = Math.ceil(
4276
4410
  Math.max(options.conversationChars ?? 0, `${systemPrompt ?? ""} ${prompt}`.length) / 4
4277
4411
  );
@@ -4690,7 +4824,7 @@ function getCostSummary() {
4690
4824
  }
4691
4825
 
4692
4826
  // src/version.ts
4693
- var SDK_VERSION = "3.21.0";
4827
+ var SDK_VERSION = "3.22.0";
4694
4828
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4695
4829
 
4696
4830
  // src/client.ts
@@ -4836,7 +4970,9 @@ var LLMClient = class _LLMClient {
4836
4970
  options?.maxOutputTokens ?? options?.maxTokens ?? DEFAULT_MAX_TOKENS,
4837
4971
  {
4838
4972
  routingProfile: options?.routingProfile,
4839
- requiresStructuredOutput: options?.responseFormat !== void 0
4973
+ requiresStructuredOutput: options?.responseFormat !== void 0,
4974
+ search: options?.search,
4975
+ searchParameters: options?.searchParameters
4840
4976
  }
4841
4977
  );
4842
4978
  }
@@ -4903,6 +5039,8 @@ var LLMClient = class _LLMClient {
4903
5039
  requiresStructuredOutput: options.responseFormat !== void 0,
4904
5040
  tools: options.tools,
4905
5041
  toolChoice: options.toolChoice,
5042
+ search: options.search,
5043
+ searchParameters: options.searchParameters,
4906
5044
  conversationChars,
4907
5045
  hasVision
4908
5046
  }
@@ -4917,6 +5055,7 @@ var LLMClient = class _LLMClient {
4917
5055
  toolChoice: options.toolChoice,
4918
5056
  responseFormat: options.responseFormat,
4919
5057
  stop: options.stop,
5058
+ timeout: options.timeout,
4920
5059
  // An explicit caller-supplied chain wins over the routed one.
4921
5060
  fallbackModels: options.fallbackModels ?? decision.fallbacks
4922
5061
  });
@@ -4993,11 +5132,11 @@ var LLMClient = class _LLMClient {
4993
5132
  if (options?.temperature !== void 0) body.temperature = options.temperature;
4994
5133
  if (options?.topP !== void 0) body.top_p = options.topP;
4995
5134
  if (options?.searchParameters !== void 0) {
4996
- body.search_parameters = options.searchParameters;
5135
+ body.search_parameters = toWireSearchParameters(options.searchParameters);
4997
5136
  } else if (options?.search === true) {
4998
5137
  body.search_parameters = { mode: "on" };
4999
5138
  }
5000
- if (options?.tools !== void 0) body.tools = options.tools;
5139
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
5001
5140
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
5002
5141
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
5003
5142
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -5008,7 +5147,7 @@ var LLMClient = class _LLMClient {
5008
5147
  for (let i = 0; i < chain.length; i++) {
5009
5148
  const candidate = chain[i];
5010
5149
  try {
5011
- return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
5150
+ return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
5012
5151
  } catch (err) {
5013
5152
  lastErr = err;
5014
5153
  const next = chain[i + 1];
@@ -5079,19 +5218,19 @@ var LLMClient = class _LLMClient {
5079
5218
  * is `"paid-or-in-doubt"`. With an API key the request itself is billed, so
5080
5219
  * only the account API's explicit 4xx refusal is `"unpaid"`.
5081
5220
  */
5082
- async requestWithPayment(endpoint, body) {
5221
+ async requestWithPayment(endpoint, body, timeoutMs) {
5083
5222
  const url = `${this.apiUrl}${endpoint}`;
5084
5223
  const unpaid = {
5085
5224
  method: "POST",
5086
5225
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
5087
5226
  body: JSON.stringify(body)
5088
5227
  };
5089
- const response = await this.sendUnpaid(url, unpaid);
5228
+ const response = await this.sendUnpaid(url, unpaid, timeoutMs);
5090
5229
  if (response.status === 502 || response.status === 503) {
5091
5230
  await new Promise((r) => setTimeout(r, 1e3));
5092
- const retryResp = await this.sendUnpaid(url, unpaid);
5231
+ const retryResp = await this.sendUnpaid(url, unpaid, timeoutMs);
5093
5232
  if (retryResp.status !== 502 && retryResp.status !== 503) {
5094
- if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp);
5233
+ if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp, timeoutMs);
5095
5234
  if (!retryResp.ok) {
5096
5235
  let errorBody;
5097
5236
  try {
@@ -5108,7 +5247,7 @@ var LLMClient = class _LLMClient {
5108
5247
  }
5109
5248
  }
5110
5249
  if (response.status === 402) {
5111
- return this.handlePaymentAndRetry(url, body, response);
5250
+ return this.handlePaymentAndRetry(url, body, response, timeoutMs);
5112
5251
  }
5113
5252
  if (!response.ok) {
5114
5253
  let errorBody;
@@ -5133,9 +5272,9 @@ var LLMClient = class _LLMClient {
5133
5272
  * {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
5134
5273
  * {@link accountErrorDisposition} (the request itself is billed).
5135
5274
  */
5136
- async sendUnpaid(url, init) {
5275
+ async sendUnpaid(url, init, timeoutMs) {
5137
5276
  try {
5138
- return await this.fetchWithTimeout(url, init);
5277
+ return await this.fetchWithTimeout(url, init, timeoutMs);
5139
5278
  } catch (err) {
5140
5279
  throw withDisposition(err, this.apiAuth ? accountErrorDisposition(err) : "unpaid");
5141
5280
  }
@@ -5143,13 +5282,22 @@ var LLMClient = class _LLMClient {
5143
5282
  /**
5144
5283
  * Parse a response to a request that carried no payment: the free tier,
5145
5284
  * or, with an API key, the billed request itself.
5285
+ *
5286
+ * With a wallet nothing was paid, so `costUsd` is 0 (a free model). With an
5287
+ * API key the call is billed to the account and `costUsd` stays undefined.
5146
5288
  */
5147
5289
  async parseUnpaidChatResponse(response) {
5290
+ let served;
5148
5291
  try {
5149
- return await this.parseChatResponse(response);
5292
+ served = await this.parseChatResponse(response);
5150
5293
  } catch (err) {
5151
5294
  throw withDisposition(err, this.apiAuth ? "paid-or-in-doubt" : "unpaid");
5152
5295
  }
5296
+ if (!this.apiAuth) {
5297
+ served.costUsd = 0;
5298
+ served.settlement = { scheme: "free", quotedUsd: 0 };
5299
+ }
5300
+ return served;
5153
5301
  }
5154
5302
  /**
5155
5303
  * Handle 402 response: parse requirements, sign payment, retry.
@@ -5157,7 +5305,7 @@ var LLMClient = class _LLMClient {
5157
5305
  * Anything thrown before the signed payment is sent is `"unpaid"`; anything
5158
5306
  * after it, `"paid-or-in-doubt"` (see {@link RetryDisposition}).
5159
5307
  */
5160
- async handlePaymentAndRetry(url, body, response) {
5308
+ async handlePaymentAndRetry(url, body, response, timeoutMs) {
5161
5309
  let signed;
5162
5310
  try {
5163
5311
  signed = await this.signFrom402(url, response);
@@ -5165,7 +5313,7 @@ var LLMClient = class _LLMClient {
5165
5313
  throw withDisposition(err, "unpaid");
5166
5314
  }
5167
5315
  try {
5168
- return await this.sendPaid(url, body, signed.paymentPayload, signed.details);
5316
+ return await this.sendPaid(url, body, signed.paymentPayload, signed.details, timeoutMs);
5169
5317
  } catch (err) {
5170
5318
  throw withDisposition(err, "paid-or-in-doubt");
5171
5319
  }
@@ -5210,7 +5358,7 @@ var LLMClient = class _LLMClient {
5210
5358
  return { paymentPayload, details };
5211
5359
  }
5212
5360
  /** Send the signed payment and read the answer. The payment may settle from here on. */
5213
- async sendPaid(url, body, paymentPayload, details) {
5361
+ async sendPaid(url, body, paymentPayload, details, timeoutMs) {
5214
5362
  const retryResponse = await this.fetchWithTimeout(url, {
5215
5363
  method: "POST",
5216
5364
  headers: {
@@ -5219,7 +5367,7 @@ var LLMClient = class _LLMClient {
5219
5367
  "PAYMENT-SIGNATURE": paymentPayload
5220
5368
  },
5221
5369
  body: JSON.stringify(body)
5222
- });
5370
+ }, timeoutMs);
5223
5371
  if (retryResponse.status === 502 || retryResponse.status === 503) {
5224
5372
  await new Promise((r) => setTimeout(r, 1e3));
5225
5373
  const retryResp2 = await this.fetchWithTimeout(url, {
@@ -5230,7 +5378,7 @@ var LLMClient = class _LLMClient {
5230
5378
  "PAYMENT-SIGNATURE": paymentPayload
5231
5379
  },
5232
5380
  body: JSON.stringify(body)
5233
- });
5381
+ }, timeoutMs);
5234
5382
  if (retryResp2.status !== 502 && retryResp2.status !== 503) {
5235
5383
  if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
5236
5384
  if (!retryResp2.ok) {
@@ -5246,7 +5394,7 @@ var LLMClient = class _LLMClient {
5246
5394
  this.sessionCalls += 1;
5247
5395
  this.sessionTotalUsd += costUsd2;
5248
5396
  this.recordCost(url, costUsd2, { body, network: details.network });
5249
- return this.parseChatResponse(retryResp2);
5397
+ return withCost(await this.parseChatResponse(retryResp2), retryResp2, costUsd2, "exact");
5250
5398
  }
5251
5399
  }
5252
5400
  if (retryResponse.status === 402) {
@@ -5269,7 +5417,7 @@ var LLMClient = class _LLMClient {
5269
5417
  this.sessionCalls += 1;
5270
5418
  this.sessionTotalUsd += costUsd;
5271
5419
  this.recordCost(url, costUsd, { body, network: details.network });
5272
- return this.parseChatResponse(retryResponse);
5420
+ return withCost(await this.parseChatResponse(retryResponse), retryResponse, costUsd, "exact");
5273
5421
  }
5274
5422
  /**
5275
5423
  * Sign a payment header and return the PAYMENT-SIGNATURE value.
@@ -5339,7 +5487,7 @@ var LLMClient = class _LLMClient {
5339
5487
  };
5340
5488
  if (options?.temperature !== void 0) body.temperature = options.temperature;
5341
5489
  if (options?.topP !== void 0) body.top_p = options.topP;
5342
- if (options?.tools !== void 0) body.tools = options.tools;
5490
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
5343
5491
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
5344
5492
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
5345
5493
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -5737,11 +5885,11 @@ var LLMClient = class _LLMClient {
5737
5885
  return retryResponse.json();
5738
5886
  }
5739
5887
  /**
5740
- * Fetch with timeout.
5888
+ * Fetch with timeout: `timeoutMs` (a per-call override), else the client's `timeout`.
5741
5889
  */
5742
- async fetchWithTimeout(url, options) {
5890
+ async fetchWithTimeout(url, options, timeoutMs = this.timeout) {
5743
5891
  const controller = new AbortController();
5744
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
5892
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
5745
5893
  try {
5746
5894
  const response = await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, {
5747
5895
  ...options,
@@ -9726,6 +9874,7 @@ var import_bs582 = __toESM(require("bs58"), 1);
9726
9874
  var BATCH_SCHEME = "batch-settlement";
9727
9875
  var BLOCKRUN_SOL_OPERATOR = "5YKPQUFjw5WQqhSUkEGKNNfYYVqnRRNbpYyL71qQ1vm3";
9728
9876
  var DEFAULT_BATCH_RATE_LIMIT = { maxAttempts: 3, maxWaitMs: 6e4 };
9877
+ var DEFAULT_BATCH_IN_DOUBT = { maxWaitMs: 3e5 };
9729
9878
  var BatchPaymentUnresolvedError = class extends PaymentError {
9730
9879
  /** The {@link SolanaBatchEvent.reason} of the `unresolved` event reported with it. */
9731
9880
  reason;
@@ -10402,6 +10551,46 @@ function withoutClientSignedBatch(paymentRequired) {
10402
10551
  const accepts = paymentRequired.accepts ?? [];
10403
10552
  return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
10404
10553
  }
10554
+ var PAYMENT_IDENTIFIER = "payment-identifier";
10555
+ function withPaymentIdentifier(paymentRequired) {
10556
+ const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
10557
+ if (typeof declared?.info?.required !== "boolean") return paymentRequired;
10558
+ const id = `pay_${(0, import_crypto2.randomUUID)().replace(/-/g, "")}`;
10559
+ return {
10560
+ ...paymentRequired,
10561
+ extensions: { ...paymentRequired.extensions, [PAYMENT_IDENTIFIER]: { ...declared, info: { ...declared.info, id } } }
10562
+ };
10563
+ }
10564
+ function peekReceipt(http, getHeader) {
10565
+ try {
10566
+ return http.getPaymentSettleResponse(getHeader);
10567
+ } catch {
10568
+ return void 0;
10569
+ }
10570
+ }
10571
+ function receiptSucceeded(http, answer) {
10572
+ return peekReceipt(http, (name) => answer.headers.get(name))?.success === true;
10573
+ }
10574
+ function hasPaymentIdentifier(payload) {
10575
+ const info = payload.extensions?.[PAYMENT_IDENTIFIER]?.info;
10576
+ return typeof info?.id === "string";
10577
+ }
10578
+ var IN_DOUBT_MIN_WAIT_MS = 1e3;
10579
+ var IN_DOUBT_MAX_WAIT_MS = 3e4;
10580
+ function replayMayResolve(doubt) {
10581
+ if (doubt.unanswered) return true;
10582
+ const answer = doubt.held;
10583
+ if (!answer) return false;
10584
+ return answer.status >= 500 || answer.status === 429 || answer.status === 409 && Boolean(answer.headers.get("retry-after")?.trim());
10585
+ }
10586
+ function inDoubtWait(doubt, replay) {
10587
+ const asked = doubt.held ? parseRetryAfter(doubt.held.headers.get("retry-after")) : void 0;
10588
+ return Math.min(IN_DOUBT_MAX_WAIT_MS, Math.max(IN_DOUBT_MIN_WAIT_MS, asked ?? backoffDelay(replay)));
10589
+ }
10590
+ function replayDoubt(outcome) {
10591
+ if (outcome.kind === "charged" || outcome.kind === "in_doubt") return outcome;
10592
+ return { kind: "in_doubt", reason: "replay_unresolved", replayable: false, settled: true, detail: "the replay was refused" };
10593
+ }
10405
10594
  function trustedBatchAccepts(paymentRequired, operators) {
10406
10595
  return (paymentRequired.accepts ?? []).filter((candidate) => {
10407
10596
  const extra = candidate.extra;
@@ -10411,9 +10600,6 @@ function trustedBatchAccepts(paymentRequired, operators) {
10411
10600
  function trustedBatchAccept(paymentRequired, operators) {
10412
10601
  return trustedBatchAccepts(paymentRequired, operators)[0];
10413
10602
  }
10414
- function atomicToUsd(amount) {
10415
- return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
10416
- }
10417
10603
  function chargedUsd(getReceipt) {
10418
10604
  let receipt;
10419
10605
  try {
@@ -10421,7 +10607,7 @@ function chargedUsd(getReceipt) {
10421
10607
  } catch {
10422
10608
  return 0;
10423
10609
  }
10424
- return atomicToUsd(receipt?.extra?.chargedAmount) ?? atomicToUsd(receipt?.amount) ?? 0;
10610
+ return receiptChargedUsd(receipt) ?? 0;
10425
10611
  }
10426
10612
  function rebuiltCharge(settled, payload) {
10427
10613
  if (settled?.success !== true) return 0n;
@@ -10541,13 +10727,42 @@ function parseRetryAfter(value, now = Date.now()) {
10541
10727
  }
10542
10728
  return Number.isFinite(ms) && ms <= MAX_RETRY_AFTER_MS ? ms : void 0;
10543
10729
  }
10544
- var NOT_BROADCAST_429 = /* @__PURE__ */ new Set([
10730
+ var NOT_BROADCAST_REFUSALS = /* @__PURE__ */ new Set([
10731
+ // 429
10545
10732
  "batch_account_channel_capacity_exhausted",
10546
10733
  "batch_channel_capacity_exhausted",
10547
- "batch_deposit_rate_limited"
10734
+ "batch_deposit_rate_limited",
10735
+ "batch_deposit_ip_rate_limited",
10736
+ // 503
10737
+ "batch_signer_funding_headroom_exhausted",
10738
+ "batch_new_channels_disabled",
10739
+ "batch_public_new_channels_disabled",
10740
+ // 403
10741
+ "batch_account_admission_paused",
10742
+ "batch_channel_owner_mismatch",
10743
+ "batch_fee_payer_unavailable",
10744
+ "batch_fee_payer_not_dedicated",
10745
+ "batch_fee_payer_lane_not_authorized",
10746
+ "batch_transaction_fee_payer_mismatch",
10747
+ // 400
10748
+ "batch_initial_deposit_out_of_range",
10749
+ "batch_deposit_asset_not_supported",
10750
+ "batch_withdraw_delay_out_of_range"
10548
10751
  ]);
10549
10752
  function provesNothingBroadcast(receipt) {
10550
- return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_429.has(receipt.errorReason);
10753
+ return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_REFUSALS.has(receipt.errorReason);
10754
+ }
10755
+ var CONFIG_REFUSALS = /* @__PURE__ */ new Set([
10756
+ "batch_initial_deposit_out_of_range",
10757
+ "batch_deposit_asset_not_supported",
10758
+ "batch_withdraw_delay_out_of_range"
10759
+ ]);
10760
+ var REFUSAL_COOLDOWN_MS = 6e4;
10761
+ var CONFIG_REFUSAL_COOLDOWN_MS = 10 * 6e4;
10762
+ var REFUSAL_COOLDOWN_MAX_MS = 15 * 6e4;
10763
+ function refusalCooldownMs(errorReason, retryAfterMs2) {
10764
+ if (retryAfterMs2 !== void 0) return Math.min(retryAfterMs2, REFUSAL_COOLDOWN_MAX_MS);
10765
+ return errorReason !== void 0 && CONFIG_REFUSALS.has(errorReason) ? CONFIG_REFUSAL_COOLDOWN_MS : REFUSAL_COOLDOWN_MS;
10551
10766
  }
10552
10767
  function backoffDelay(attempt) {
10553
10768
  const base = 1e3 * 2 ** Math.max(0, attempt - 1);
@@ -10680,7 +10895,7 @@ function intentTarget(intent) {
10680
10895
  var SolanaBatchPayer = class {
10681
10896
  constructor(init) {
10682
10897
  this.init = init;
10683
- const { operators, rateLimit } = init.options;
10898
+ const { operators, rateLimit, inDoubt } = init.options;
10684
10899
  if (!Array.isArray(operators) || operators.length === 0 || operators.some((o) => typeof o !== "string" || !o)) {
10685
10900
  throw new Error("batch.operators must list at least one base58 operator public key");
10686
10901
  }
@@ -10692,11 +10907,16 @@ var SolanaBatchPayer = class {
10692
10907
  if (!Number.isFinite(this.maxWaitMs) || this.maxWaitMs < 0) {
10693
10908
  throw new Error("batch.rateLimit.maxWaitMs must be a finite number of ms >= 0");
10694
10909
  }
10910
+ this.inDoubtMaxWaitMs = inDoubt?.maxWaitMs ?? DEFAULT_BATCH_IN_DOUBT.maxWaitMs;
10911
+ if (!Number.isFinite(this.inDoubtMaxWaitMs) || this.inDoubtMaxWaitMs < 0) {
10912
+ throw new Error("batch.inDoubt.maxWaitMs must be a finite number of ms >= 0");
10913
+ }
10695
10914
  }
10696
10915
  init;
10697
10916
  state;
10698
10917
  maxAttempts;
10699
10918
  maxWaitMs;
10919
+ inDoubtMaxWaitMs;
10700
10920
  counters = {
10701
10921
  fallbacks: 0,
10702
10922
  fallbacksByReason: {},
@@ -11065,14 +11285,19 @@ var SolanaBatchPayer = class {
11065
11285
  * the call's result (`served`, nothing paid); one that fails otherwise
11066
11286
  * is the call's error (`failed`, `"unpaid"`), never an exact payment
11067
11287
  * against the stale challenge;
11068
- * - in doubt: ends only with a definitive success receipt from the one
11069
- * replay a receipt-less 429 is allowed ({@link resolveInDoubt}), or a
11070
- * thrown {@link BatchPaymentUnresolvedError}. No new authorization, no
11071
- * new deposit, no exact payment and no fallback model is ever paid for a
11072
- * call in doubt.
11288
+ * - in doubt: ends only with a definitive success receipt from a replay
11289
+ * of the identical payment ({@link resolveInDoubt}: the one replay a
11290
+ * receipt-less 429 is allowed, or the replays of a payment carrying a
11291
+ * payment identifier while the gateway says one can succeed, within
11292
+ * `inDoubt.maxWaitMs`), or a thrown {@link BatchPaymentUnresolvedError}.
11293
+ * No new authorization, no new deposit, no exact payment and no fallback
11294
+ * model is ever paid for a call in doubt.
11073
11295
  *
11074
11296
  * The wallet stays in cooldown after a 429, so its other calls wait
11075
- * instead of sending opens of their own.
11297
+ * instead of sending opens of their own. After a deposit the facilitator
11298
+ * refused before broadcasting it (a 402 with a failed `batch_*` admission
11299
+ * receipt), its calls do not wait: they pay exact at once
11300
+ * (`admission_refused`) until the refusal cooldown ends.
11076
11301
  *
11077
11302
  * @param closesAtStart - {@link closeFence}, taken before the call's first
11078
11303
  * (unpaid) request. If a close completed since, the call pays exact
@@ -11136,6 +11361,15 @@ var SolanaBatchPayer = class {
11136
11361
  try {
11137
11362
  for (; ; ) {
11138
11363
  const now = Date.now();
11364
+ if ((wallet.refusedUntil ?? 0) > now) {
11365
+ return fallback({
11366
+ reason: "admission_refused",
11367
+ errorReason: wallet.refusedFor,
11368
+ retryAfterMs: (wallet.refusedUntil ?? 0) - now,
11369
+ attempt: budget.attempt,
11370
+ detail: `the facilitator refused this wallet's last batch deposit (${wallet.refusedFor ?? "unknown"}); batch is skipped until the refusal cooldown ends`
11371
+ });
11372
+ }
11139
11373
  if (wallet.cooldownUntil > now && wallet.cooldownUntil !== waitedUntil) {
11140
11374
  const wait = wallet.cooldownUntil - now;
11141
11375
  if (budget.waited + wait > this.maxWaitMs) {
@@ -11225,12 +11459,23 @@ var SolanaBatchPayer = class {
11225
11459
  if (built.kind !== "authorization") {
11226
11460
  if (outcome.depositSafe) this.forgetIntent(wallet, built.key);
11227
11461
  else this.distrust(wallet, built, "deposit_refused");
11462
+ if (outcome.refusedForMs !== void 0) {
11463
+ const until = Date.now() + outcome.refusedForMs;
11464
+ if (until > (wallet.refusedUntil ?? 0)) {
11465
+ wallet.refusedUntil = until;
11466
+ wallet.refusedFor = outcome.errorReason;
11467
+ }
11468
+ }
11228
11469
  }
11229
11470
  sent = void 0;
11230
11471
  release();
11231
11472
  if (!outcome.rateLimited) {
11232
- const { kind: _kind, rateLimited: _limited, depositSafe: _safe, ...event } = outcome;
11233
- return fallback({ ...event, attempt: budget.attempt });
11473
+ const { kind: _kind, rateLimited: _limited, depositSafe: _safe, refusedForMs, ...event } = outcome;
11474
+ return fallback({
11475
+ ...event,
11476
+ ...refusedForMs !== void 0 && built.kind !== "authorization" ? { retryAfterMs: refusedForMs } : {},
11477
+ attempt: budget.attempt
11478
+ });
11234
11479
  }
11235
11480
  const wait = this.coolDown(wallet, outcome.rateLimited.retryAfterMs ?? backoffDelay(budget.attempt));
11236
11481
  if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
@@ -11276,7 +11521,7 @@ var SolanaBatchPayer = class {
11276
11521
  if (built.kind !== "authorization" && !(built.key && wallet.resyncs.has(built.key))) this.forgetIntent(wallet, built.key);
11277
11522
  sent = void 0;
11278
11523
  release();
11279
- await this.report({ type: "recovered", reason: "rate_limited", attempt: budget.attempt });
11524
+ await this.report({ type: "recovered", reason: outcome.status === 429 ? "rate_limited" : "in_doubt", attempt: budget.attempt });
11280
11525
  return { kind: "paid", response: resolved.response, chargedUsd: resolved.chargedUsd };
11281
11526
  }
11282
11527
  return await this.raiseUnresolved(wallet, built, resolved, budget.attempt);
@@ -11325,21 +11570,24 @@ var SolanaBatchPayer = class {
11325
11570
  /**
11326
11571
  * Try to end a payment in doubt with the gateway's own word.
11327
11572
  *
11328
- * The one source of evidence this SDK accepts today is a definitive
11329
- * success receipt on the single byte-identical replay a 429 without a
11330
- * receipt is allowed, after its backoff (the owner's bounded safety
11331
- * attempt). Any other answer to it (a 402 or `duplicate_settlement`, which
11332
- * mean the original reached the gateway; another 429; a 5xx; a timeout)
11333
- * leaves it in doubt. Every other in-doubt outcome is never replayed.
11573
+ * The one source of evidence this SDK accepts is a definitive success
11574
+ * receipt on a byte-identical replay of the payment. Any other answer to a
11575
+ * replay (a 402 or `duplicate_settlement`, which mean the original reached
11576
+ * the gateway; a 409; another 429; a 5xx; a timeout) leaves it in doubt.
11577
+ *
11578
+ * - A payment carrying a payment identifier is replayed for as long as
11579
+ * the gateway says a later replay can succeed ({@link replayInDoubt}).
11580
+ * - Any other payment gets a replay only after a 429 without a receipt:
11581
+ * one, after its backoff (the owner's bounded safety attempt), within
11582
+ * `rateLimit`. Every other in-doubt outcome is never replayed.
11334
11583
  *
11335
11584
  * Chain state is not evidence here: it can settle a deposit (see
11336
- * `resync`), never whether the gateway charged. Gateway-side evidence (a
11337
- * receipt on every response, a request-status endpoint with a fence)
11338
- * plugs in here, before the caller raises.
11585
+ * `resync`), never whether the gateway charged.
11339
11586
  *
11340
11587
  * @returns the charged replay, or the doubt to raise.
11341
11588
  */
11342
11589
  async resolveInDoubt(wallet, sent, doubt, send, budget) {
11590
+ if (hasPaymentIdentifier(sent.payload)) return this.replayInDoubt(wallet, sent, doubt, send, budget.attempt);
11343
11591
  if (!doubt.replayable) return doubt;
11344
11592
  const wait = this.coolDown(wallet, doubt.retryAfterMs ?? backoffDelay(budget.attempt));
11345
11593
  if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
@@ -11359,9 +11607,94 @@ var SolanaBatchPayer = class {
11359
11607
  budget.waited += wait;
11360
11608
  await sleep2(wait);
11361
11609
  budget.attempt += 1;
11362
- const replayed = await this.sendOnce(wallet, sent, send, true, true);
11363
- if (replayed.kind === "charged") return replayed;
11364
- return replayed.kind === "in_doubt" ? replayed : { kind: "in_doubt", reason: "replay_unresolved", replayable: false, settled: true, detail: "the replay was refused" };
11610
+ return replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
11611
+ }
11612
+ /**
11613
+ * Replay a payment in doubt that carries a payment identifier until the
11614
+ * gateway's answer settles it.
11615
+ *
11616
+ * A gateway that declares `payment-identifier` stores each paid response
11617
+ * under its identifier and answers a byte-identical replay of a completed
11618
+ * request with it, the original receipt included; while the original is
11619
+ * still running, it answers 409 with a `Retry-After`. So the identical
11620
+ * payment (the same `PAYMENT-SIGNATURE` header and the same body) is
11621
+ * replayed for as long as the last answer says a later one can succeed
11622
+ * ({@link replayMayResolve}), after {@link inDoubtWait} each time, as long
11623
+ * as the wait fits in `inDoubt.maxWaitMs`, counted from the first doubt:
11624
+ * every wait (as long as it really took) and every replay's round trip. A
11625
+ * replay already sent is not cut short. These replays do not count
11626
+ * against `rateLimit`; a 429 among them still puts the wallet in cooldown.
11627
+ *
11628
+ * Every answer that does not settle it is held back from the scheme
11629
+ * ({@link InDoubt.held}) and handed over only once no replay follows, so
11630
+ * the scheme rolls the payment back once, or takes the receipt that answer
11631
+ * carries. That is the last answer, unless an earlier one carried a
11632
+ * success receipt and the later ones do not: a success receipt is the
11633
+ * gateway's word that it charged, and handing the scheme a receipt-less
11634
+ * refusal of a replay instead would roll a real charge back and desync the
11635
+ * channel's cumulative.
11636
+ *
11637
+ * @returns the charged replay, or the doubt to raise.
11638
+ */
11639
+ async replayInDoubt(wallet, sent, first, send, attempt) {
11640
+ let doubt = first;
11641
+ let held = first.held;
11642
+ let heldSucceeded = held !== void 0 && receiptSucceeded(sent.http, held);
11643
+ let replays = 0;
11644
+ let spent = 0;
11645
+ const stop = async (note) => {
11646
+ if (!doubt.settled && held) {
11647
+ const answer = held;
11648
+ await this.settle(sent.http, sent.payload, (name) => answer.headers.get(name), answer.status);
11649
+ }
11650
+ const notes = [replays > 0 ? `${replays} replay${replays === 1 ? "" : "s"} did not resolve it` : "", note ?? ""];
11651
+ return {
11652
+ ...doubt,
11653
+ settled: doubt.settled || held !== void 0,
11654
+ held: void 0,
11655
+ firstReason: first.reason,
11656
+ detail: [doubt.detail, ...notes].filter(Boolean).join("; ")
11657
+ };
11658
+ };
11659
+ for (; ; ) {
11660
+ if (!replayMayResolve(doubt)) {
11661
+ return stop(doubt.held?.status === 409 ? "the gateway sent no Retry-After, so no replay can resolve it" : void 0);
11662
+ }
11663
+ const wait = inDoubtWait(doubt, replays + 1);
11664
+ const status2 = doubt.held?.status;
11665
+ if (status2 === 429) this.coolDown(wallet, (doubt.held && parseRetryAfter(doubt.held.headers.get("retry-after"))) ?? wait);
11666
+ if (spent + wait > this.inDoubtMaxWaitMs) {
11667
+ return stop(`batch.inDoubt.maxWaitMs (${this.inDoubtMaxWaitMs} ms) leaves no room for ${replays > 0 ? "another replay" : "a replay"}`);
11668
+ }
11669
+ await this.report({
11670
+ type: "backoff",
11671
+ reason: status2 === 429 ? "rate_limited" : "in_doubt",
11672
+ status: status2,
11673
+ errorReason: doubt.errorReason,
11674
+ retryAfterMs: wait,
11675
+ attempt,
11676
+ detail: `replay ${replays + 1} of the payment in doubt`
11677
+ });
11678
+ const sleptFrom = Date.now();
11679
+ await sleep2(wait);
11680
+ spent += Math.max(wait, Date.now() - sleptFrom);
11681
+ replays += 1;
11682
+ const startedAt = Date.now();
11683
+ const replayed = replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
11684
+ spent += Math.max(0, Date.now() - startedAt);
11685
+ if (replayed.kind === "charged") return replayed;
11686
+ doubt = replayed;
11687
+ if (doubt.held) {
11688
+ const succeeded = receiptSucceeded(sent.http, doubt.held);
11689
+ if (succeeded || !heldSucceeded) {
11690
+ held = doubt.held;
11691
+ heldSucceeded = succeeded;
11692
+ }
11693
+ } else if (doubt.settled) {
11694
+ held = void 0;
11695
+ heldSucceeded = false;
11696
+ }
11697
+ }
11365
11698
  }
11366
11699
  /**
11367
11700
  * Raise a payment in doubt: release the scheme's hold on it (the channel
@@ -11372,10 +11705,11 @@ var SolanaBatchPayer = class {
11372
11705
  async raiseUnresolved(wallet, sent, doubt, attempt) {
11373
11706
  if (!doubt.settled) await this.settle(sent.http, sent.payload, () => null, 0);
11374
11707
  if (sent.kind !== "authorization") {
11708
+ const reason = doubt.firstReason ?? doubt.reason;
11375
11709
  this.distrust(
11376
11710
  wallet,
11377
11711
  sent,
11378
- doubt.reason === "no_response" ? "deposit_unanswered" : doubt.reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
11712
+ reason === "no_response" ? "deposit_unanswered" : reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
11379
11713
  );
11380
11714
  }
11381
11715
  await this.report({
@@ -11462,8 +11796,8 @@ var SolanaBatchPayer = class {
11462
11796
  * here, and only for a first send.
11463
11797
  *
11464
11798
  * @param retry - count this send in `retries` (it follows this call's own 429).
11465
- * @param replay - this is the one replay of a receipt-less 429: only a 2xx
11466
- * with a success receipt counts, everything else stays in doubt.
11799
+ * @param replay - this is a replay of a payment in doubt: only a 2xx with
11800
+ * a success receipt counts, everything else stays in doubt.
11467
11801
  */
11468
11802
  async sendOnce(wallet, sent, send, retry, replay) {
11469
11803
  if (retry) this.counters.retries += 1;
@@ -11477,6 +11811,7 @@ var SolanaBatchPayer = class {
11477
11811
  reason: replay ? "replay_unresolved" : "no_response",
11478
11812
  replayable: false,
11479
11813
  settled: false,
11814
+ unanswered: true,
11480
11815
  detail: `${replay ? "the replay" : "the payment"} got no answer (${err instanceof Error ? `${err.name}: ${err.message}` : String(err)})`,
11481
11816
  cause: err
11482
11817
  };
@@ -11501,6 +11836,7 @@ var SolanaBatchPayer = class {
11501
11836
  const getHeader = (name) => response.headers.get(name);
11502
11837
  const receipted = Boolean(getHeader("PAYMENT-RESPONSE") || getHeader("X-PAYMENT-RESPONSE"));
11503
11838
  const status2 = response.status;
11839
+ const hold = hasPaymentIdentifier(payload) && (!response.ok || replay && peekReceipt(http, getHeader)?.success !== true);
11504
11840
  if (status2 === 429 && !receipted) {
11505
11841
  return {
11506
11842
  kind: "in_doubt",
@@ -11510,12 +11846,27 @@ var SolanaBatchPayer = class {
11510
11846
  status: status2,
11511
11847
  retryAfterMs: parseRetryAfter(getHeader("retry-after")),
11512
11848
  errorReason: await rateLimitReason(response, void 0),
11513
- detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt"
11849
+ detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt",
11850
+ ...hold ? { held: response } : {}
11514
11851
  };
11515
11852
  }
11853
+ const outcome = await this.classifyAnswer(wallet, sent, response, replay, hold);
11854
+ if (!hold) return outcome;
11855
+ if (outcome.kind === "in_doubt") return { ...outcome, settled: false, held: response };
11856
+ await this.settle(http, payload, getHeader, status2);
11857
+ return outcome;
11858
+ }
11859
+ /**
11860
+ * {@link classify}, past a receipt-less 429. With `hold`, the answer is not
11861
+ * handed to the scheme: its receipt is only read.
11862
+ */
11863
+ async classifyAnswer(wallet, sent, response, replay, hold) {
11864
+ const { http, payload } = sent;
11865
+ const getHeader = (name) => response.headers.get(name);
11866
+ const status2 = response.status;
11516
11867
  const channelId = payloadChannelId(payload);
11517
11868
  const before = channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0;
11518
- const reconciled = await this.settle(http, payload, getHeader, status2);
11869
+ const reconciled = hold ? { settleResponse: peekReceipt(http, getHeader), ok: false } : await this.settle(http, payload, getHeader, status2);
11519
11870
  const receipt = reconciled.settleResponse;
11520
11871
  const definitive = receipt?.success === true && reconciled.ok && recordAdvanced(before, channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0, payload, receipt);
11521
11872
  if (!response.ok && !replay && sent.kind === "authorization" && cancelledReceipt(receipt)) {
@@ -11578,7 +11929,15 @@ var SolanaBatchPayer = class {
11578
11929
  cause: await afterPaymentError(response)
11579
11930
  };
11580
11931
  }
11581
- return { kind: "not_charged", reason: "payment_required", status: status2, errorReason: receipt?.errorReason, depositSafe: provesNothingBroadcast(receipt) };
11932
+ const refused = provesNothingBroadcast(receipt);
11933
+ return {
11934
+ kind: "not_charged",
11935
+ reason: "payment_required",
11936
+ status: status2,
11937
+ errorReason: receipt?.errorReason,
11938
+ depositSafe: refused,
11939
+ ...refused ? { refusedForMs: refusalCooldownMs(receipt?.errorReason, parseRetryAfter(getHeader("retry-after"))) } : {}
11940
+ };
11582
11941
  }
11583
11942
  const refusal = await batchRefusal(response);
11584
11943
  if (refusal) {
@@ -11609,7 +11968,7 @@ var SolanaBatchPayer = class {
11609
11968
  const http = (await this.build(wallet)).http;
11610
11969
  try {
11611
11970
  await this.prepareTopUp(wallet, paymentRequired);
11612
- return { http, payload: await http.createPaymentPayload(withoutClientSignedBatch(paymentRequired)) };
11971
+ return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(withoutClientSignedBatch(paymentRequired))) };
11613
11972
  } catch (err) {
11614
11973
  if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
11615
11974
  } finally {
@@ -11702,13 +12061,7 @@ var SolanaBatchPayer = class {
11702
12061
  console.error(
11703
12062
  `[@blockrun/llm] batch-settlement receipt not reconciled: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
11704
12063
  );
11705
- let settleResponse;
11706
- try {
11707
- settleResponse = http.getPaymentSettleResponse(getHeader);
11708
- } catch {
11709
- settleResponse = void 0;
11710
- }
11711
- return { settleResponse, ok: false };
12064
+ return { settleResponse: peekReceipt(http, getHeader), ok: false };
11712
12065
  }
11713
12066
  }
11714
12067
  /**
@@ -12091,9 +12444,9 @@ var SolanaLLMClient = class {
12091
12444
  };
12092
12445
  if (options?.temperature !== void 0) body.temperature = options.temperature;
12093
12446
  if (options?.topP !== void 0) body.top_p = options.topP;
12094
- if (options?.searchParameters !== void 0) body.search_parameters = options.searchParameters;
12447
+ if (options?.searchParameters !== void 0) body.search_parameters = toWireSearchParameters(options.searchParameters);
12095
12448
  else if (options?.search === true) body.search_parameters = { mode: "on" };
12096
- if (options?.tools !== void 0) body.tools = options.tools;
12449
+ if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
12097
12450
  if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
12098
12451
  if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
12099
12452
  if (options?.stop !== void 0) body.stop = options.stop;
@@ -12104,7 +12457,7 @@ var SolanaLLMClient = class {
12104
12457
  for (let index = 0; index < chain.length; index += 1) {
12105
12458
  const candidate = chain[index];
12106
12459
  try {
12107
- return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate));
12460
+ return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
12108
12461
  } catch (error) {
12109
12462
  lastError = error;
12110
12463
  if (!isTransientError(error) || index === chain.length - 1) throw error;
@@ -12154,6 +12507,8 @@ var SolanaLLMClient = class {
12154
12507
  {
12155
12508
  routingProfile: options?.routingProfile,
12156
12509
  requiresStructuredOutput: options?.responseFormat !== void 0,
12510
+ search: options?.search,
12511
+ searchParameters: options?.searchParameters,
12157
12512
  minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
12158
12513
  }
12159
12514
  );
@@ -12181,6 +12536,8 @@ var SolanaLLMClient = class {
12181
12536
  requiresStructuredOutput: options.responseFormat !== void 0,
12182
12537
  tools: options.tools,
12183
12538
  toolChoice: options.toolChoice,
12539
+ search: options.search,
12540
+ searchParameters: options.searchParameters,
12184
12541
  conversationChars,
12185
12542
  hasVision,
12186
12543
  minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
@@ -12196,6 +12553,7 @@ var SolanaLLMClient = class {
12196
12553
  toolChoice: options.toolChoice,
12197
12554
  responseFormat: options.responseFormat,
12198
12555
  stop: options.stop,
12556
+ timeout: options.timeout,
12199
12557
  // An explicit caller-supplied chain wins over the routed one.
12200
12558
  fallbackModels: options.fallbackModels ?? decision.fallbacks
12201
12559
  });
@@ -12333,7 +12691,7 @@ var SolanaLLMClient = class {
12333
12691
  *
12334
12692
  * @returns USDC balance as a float
12335
12693
  */
12336
- async getBalance() {
12694
+ async getBalance(options) {
12337
12695
  const usdc_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
12338
12696
  const address = await this.getWalletAddress();
12339
12697
  try {
@@ -12348,6 +12706,9 @@ var SolanaLLMClient = class {
12348
12706
  })
12349
12707
  });
12350
12708
  const data = await response.json();
12709
+ if (options?.strict && (!response.ok || data.error)) {
12710
+ throw new Error(`Solana RPC balance lookup failed: ${data.error?.message ?? `HTTP ${response.status}`}`);
12711
+ }
12351
12712
  const accounts = data.result?.value || [];
12352
12713
  if (!accounts.length) return 0;
12353
12714
  let total = 0;
@@ -12355,7 +12716,8 @@ var SolanaLLMClient = class {
12355
12716
  total += acct.account?.data?.parsed?.info?.tokenAmount?.uiAmount || 0;
12356
12717
  }
12357
12718
  return total;
12358
- } catch {
12719
+ } catch (error) {
12720
+ if (options?.strict) throw error;
12359
12721
  return 0;
12360
12722
  }
12361
12723
  }
@@ -12630,15 +12992,16 @@ var SolanaLLMClient = class {
12630
12992
  * `"paid-or-in-doubt"`, and so is any error with an API key once the billed
12631
12993
  * request was sent, except the account API's explicit 4xx refusal.
12632
12994
  */
12633
- async requestWithPayment(endpoint, body) {
12995
+ async requestWithPayment(endpoint, body, timeoutMs) {
12634
12996
  const url = `${this.apiUrl}${endpoint}`;
12997
+ const json = JSON.stringify(body);
12635
12998
  const closesAtStart = await this.batchPayer?.closeFence();
12636
12999
  for (let staleRetries = 0; ; ) {
12637
13000
  const response = await this.sendUnpaid(url, {
12638
13001
  method: "POST",
12639
13002
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
12640
- body: JSON.stringify(body)
12641
- });
13003
+ body: json
13004
+ }, timeoutMs);
12642
13005
  if (response.status === 402) {
12643
13006
  try {
12644
13007
  let paymentRequired;
@@ -12654,16 +13017,16 @@ var SolanaLLMClient = class {
12654
13017
  (paymentHeaders) => this.fetchWithTimeout(url, {
12655
13018
  method: "POST",
12656
13019
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...paymentHeaders },
12657
- body: JSON.stringify(body)
12658
- }),
13020
+ body: json
13021
+ }, timeoutMs),
12659
13022
  // A fresh, unpaid challenge after a 429 wait: the request itself,
12660
13023
  // sent again without a payment. Every error it throws is unpaid.
12661
13024
  async () => {
12662
13025
  const challenge = await this.sendUnpaid(url, {
12663
13026
  method: "POST",
12664
13027
  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
12665
- body: JSON.stringify(body)
12666
- });
13028
+ body: json
13029
+ }, timeoutMs);
12667
13030
  if (challenge.status === 402) {
12668
13031
  try {
12669
13032
  return { kind: "challenge", paymentRequired: await this.readPaymentRequired(challenge) };
@@ -12678,13 +13041,14 @@ var SolanaLLMClient = class {
12678
13041
  );
12679
13042
  if (batch.kind === "paid") {
12680
13043
  this.recordSettlement(batch.chargedUsd);
12681
- return await this.readPaidJson(batch.response);
13044
+ const paid = await this.readPaidJson(batch.response);
13045
+ return withCost(paid, batch.response, batch.chargedUsd, "batch");
12682
13046
  }
12683
- if (batch.kind === "served") return await this.readUnpaidJson(batch.response);
13047
+ if (batch.kind === "served") return this.withUnpaidCost(await this.readUnpaidJson(batch.response));
12684
13048
  if (batch.kind === "failed") throw batch.error;
12685
13049
  exactRequired = batch.paymentRequired ?? paymentRequired;
12686
13050
  }
12687
- return await this.handlePaymentAndRetry(url, body, exactRequired, staleRetries > 0);
13051
+ return await this.handlePaymentAndRetry(url, json, exactRequired, staleRetries > 0, timeoutMs);
12688
13052
  } catch (error) {
12689
13053
  if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
12690
13054
  await waitForStaleRetry(staleRetries++);
@@ -12692,9 +13056,21 @@ var SolanaLLMClient = class {
12692
13056
  }
12693
13057
  }
12694
13058
  if (!response.ok) throw await unpaidApiError(response);
12695
- return await this.readUnpaidJson(response);
13059
+ return this.withUnpaidCost(await this.readUnpaidJson(response));
12696
13060
  }
12697
13061
  }
13062
+ /**
13063
+ * Cost of a chat response served without a payment: 0 with a wallet (a free
13064
+ * model). With an API key the call is billed to the account, so costUsd stays
13065
+ * undefined (see the dashboard).
13066
+ */
13067
+ withUnpaidCost(served) {
13068
+ if (!this.apiAuth) {
13069
+ served.costUsd = 0;
13070
+ served.settlement = { scheme: "free", quotedUsd: 0 };
13071
+ }
13072
+ return served;
13073
+ }
12698
13074
  /**
12699
13075
  * Read the JSON body of a 2xx served without a payment: the free tier, or
12700
13076
  * with an API key, where this very request was the billed one.
@@ -12711,9 +13087,9 @@ var SolanaLLMClient = class {
12711
13087
  * {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
12712
13088
  * {@link accountErrorDisposition} (the request itself is billed).
12713
13089
  */
12714
- async sendUnpaid(url, init) {
13090
+ async sendUnpaid(url, init, timeoutMs) {
12715
13091
  try {
12716
- return await this.fetchWithTimeout(url, init);
13092
+ return await this.fetchWithTimeout(url, init, timeoutMs);
12717
13093
  } catch (error) {
12718
13094
  throw withDisposition(error, this.apiAuth ? accountErrorDisposition(error) : "unpaid");
12719
13095
  }
@@ -12982,7 +13358,7 @@ var SolanaLLMClient = class {
12982
13358
  if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
12983
13359
  const fromAddress = await this.getWalletAddress();
12984
13360
  const secretKey = await solanaKeyToBytes(this.privateKey);
12985
- const extensions = paymentRequired.extensions;
13361
+ const extensions = withPaymentIdentifier(paymentRequired).extensions;
12986
13362
  const paymentPayload = await createSolanaPaymentPayload(
12987
13363
  secretKey,
12988
13364
  fromAddress,
@@ -13005,7 +13381,7 @@ var SolanaLLMClient = class {
13005
13381
  );
13006
13382
  return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
13007
13383
  }
13008
- async handlePaymentAndRetry(url, body, paymentRequired, forceFreshBlockhash = false) {
13384
+ async handlePaymentAndRetry(url, json, paymentRequired, forceFreshBlockhash = false, timeoutMs) {
13009
13385
  let signed;
13010
13386
  try {
13011
13387
  signed = await this.signExactPayment(url, paymentRequired, forceFreshBlockhash, `${this.apiUrl}/v1/chat/completions`);
@@ -13022,14 +13398,15 @@ var SolanaLLMClient = class {
13022
13398
  "User-Agent": USER_AGENT,
13023
13399
  "PAYMENT-SIGNATURE": paymentPayload
13024
13400
  },
13025
- body: JSON.stringify(body)
13026
- });
13401
+ body: json
13402
+ }, timeoutMs);
13027
13403
  } catch (error) {
13028
13404
  throw withDisposition(error, "paid-or-in-doubt");
13029
13405
  }
13030
13406
  await this.assertPaid(retryResponse);
13031
13407
  this.recordSettlement(costUsd);
13032
- return this.readPaidJson(retryResponse);
13408
+ const paid = await this.readPaidJson(retryResponse);
13409
+ return withCost(paid, retryResponse, costUsd, "exact");
13033
13410
  }
13034
13411
  async requestWithPaymentRaw(endpoint, body, media = {}) {
13035
13412
  const url = `${this.apiUrl}${endpoint}`;
@@ -13745,6 +14122,7 @@ var AnthropicClient = class {
13745
14122
  BatchPaymentUnresolvedError,
13746
14123
  BlockrunClient,
13747
14124
  BlockrunError,
14125
+ DEFAULT_BATCH_IN_DOUBT,
13748
14126
  DEFAULT_BATCH_RATE_LIMIT,
13749
14127
  EVM_NETWORKS,
13750
14128
  ImageClient,
@@ -13807,6 +14185,7 @@ var AnthropicClient = class {
13807
14185
  loadWallet,
13808
14186
  logCost,
13809
14187
  parsePaymentRequired,
14188
+ readSettlement,
13810
14189
  retryDisposition,
13811
14190
  saveSolanaWallet,
13812
14191
  saveToCache,
@@ -13821,6 +14200,8 @@ var AnthropicClient = class {
13821
14200
  solanaKeyToBytes,
13822
14201
  solanaPublicKey,
13823
14202
  status,
14203
+ toWireSearchParameters,
14204
+ toWireTools,
13824
14205
  validateMaxTokens,
13825
14206
  validateModel,
13826
14207
  validateTemperature,